From c5d282d203cdaad8ab7db1c882aefdd6256175d2 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Thu, 20 Nov 2025 00:09:10 +0100 Subject: [PATCH 01/67] Mathlive --- packages/ckeditor5-math/package.json | 3 +- packages/ckeditor5-math/src/index.ts | 2 + packages/ckeditor5-math/src/mathui.ts | 19 +- .../ckeditor5-math/src/ui/mainformview.ts | 151 +++++++---- .../src/ui/mathliveinputview.ts | 105 ++++++++ .../src/ui/rawlatexinputview.ts | 61 +++++ packages/ckeditor5-math/tests/mathui.ts | 6 +- .../ckeditor5-math/theme/icons/cancel.svg | 4 + packages/ckeditor5-math/theme/icons/check.svg | 3 + packages/ckeditor5-math/theme/mathform.css | 238 ++++++++++++++++-- pnpm-lock.yaml | 196 ++++++++------- 11 files changed, 610 insertions(+), 178 deletions(-) create mode 100644 packages/ckeditor5-math/src/ui/mathliveinputview.ts create mode 100644 packages/ckeditor5-math/src/ui/rawlatexinputview.ts create mode 100644 packages/ckeditor5-math/theme/icons/cancel.svg create mode 100644 packages/ckeditor5-math/theme/icons/check.svg diff --git a/packages/ckeditor5-math/package.json b/packages/ckeditor5-math/package.json index 5a18b797d..583a448cc 100644 --- a/packages/ckeditor5-math/package.json +++ b/packages/ckeditor5-math/package.json @@ -71,6 +71,7 @@ ] }, "dependencies": { - "@ckeditor/ckeditor5-icons": "47.2.0" + "@ckeditor/ckeditor5-icons": "47.2.0", + "mathlive": "0.108.2" } } diff --git a/packages/ckeditor5-math/src/index.ts b/packages/ckeditor5-math/src/index.ts index 6d6982ae2..b3475309f 100644 --- a/packages/ckeditor5-math/src/index.ts +++ b/packages/ckeditor5-math/src/index.ts @@ -1,6 +1,8 @@ import ckeditor from './../theme/icons/math.svg?raw'; import './augmentation.js'; import "../theme/mathform.css"; +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 4c4a2794c..2ac4c79cd 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -4,6 +4,7 @@ import mathIcon from '../theme/icons/math.svg?raw'; import { Plugin, ClickObserver, ButtonView, ContextualBalloon, clickOutsideHandler, CKEditorError, uid } from 'ckeditor5'; import { getBalloonPositionData } from './utils.js'; import MathCommand from './mathcommand.js'; +import 'mathlive'; const mathKeystroke = 'Ctrl+M'; @@ -56,7 +57,7 @@ export default class MathUI extends Plugin { this._balloon.showStack( 'main' ); requestAnimationFrame(() => { - this.formView?.mathInputView.fieldView.element?.focus(); + this.formView?.mathInputView.focus(); }); } @@ -89,7 +90,7 @@ export default class MathUI extends Plugin { mathConfig.katexRenderOptions! ); - formView.mathInputView.bind( 'value' ).to( mathCommand, 'value' ); + formView.mathInputView.bind( 'value' ).to( mathCommand, value => value ?? '' ); formView.displayButtonView.bind( 'isOn' ).to( mathCommand, 'display' ); // Form elements should be read-only when corresponding commands are disabled. @@ -122,18 +123,6 @@ export default class MathUI extends Plugin { } }); - // 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,7 +151,7 @@ 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 diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 2d1c59793..ef818e203 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -1,23 +1,18 @@ -import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } 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 { 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'; -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 mathInputView: MathLiveInputView; + public rawLatexInputView: RawLatexInputView; + public rawLatexLabel: LabelView; public displayButtonView: SwitchButtonView; public cancelButtonView: ButtonView; public previewEnabled: boolean; @@ -54,6 +49,13 @@ export default class MainFormView extends View { // Equation input this.mathInputView = this._createMathInput(); + // 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(); @@ -74,6 +76,8 @@ export default class MainFormView extends View { children = [ this.mathInputView, + this.rawLatexLabel, + this.rawLatexInputView, this.displayButtonView, this.previewLabel, this.mathView @@ -81,6 +85,8 @@ export default class MainFormView extends View { } else { children = [ this.mathInputView, + this.rawLatexLabel, + this.rawLatexInputView, this.displayButtonView ]; } @@ -101,14 +107,28 @@ export default class MainFormView extends View { { tag: 'div', attributes: { - class: [ - 'ck-math-view' - ] + class: [ 'ck-math-scroll' ] }, - children + children: [ + { + tag: 'div', + attributes: { + class: [ 'ck-math-view' ] + }, + children + } + ] }, - this.saveButtonView, - this.cancelButtonView + { + tag: 'div', + attributes: { + class: [ 'ck-math-button-row' ] + }, + children: [ + this.saveButtonView, + this.cancelButtonView + ] + } ] } ); } @@ -124,6 +144,7 @@ export default class MainFormView extends View { // Register form elements to focusable elements const childViews = [ this.mathInputView, + this.rawLatexInputView, this.displayButtonView, this.saveButtonView, this.cancelButtonView @@ -147,13 +168,12 @@ export default class MainFormView extends View { } public get equation(): string { - return this.mathInputView.fieldView.element?.value ?? ''; + return this.mathInputView.value ?? ''; } public set equation( equation: string ) { - if ( this.mathInputView.fieldView.element ) { - this.mathInputView.fieldView.element.value = equation; - } + this.mathInputView.value = equation; + this.rawLatexInputView.value = equation; if ( this.previewEnabled && this.mathView ) { this.mathView.value = equation; } @@ -172,46 +192,79 @@ export default class MainFormView extends View { } } ); + /** + * Creates the MathLive visual equation editor. + * + * Handles bidirectional synchronization with the raw LaTeX input and preview. + */ 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 mathInput = new MathLiveInputView( this.locale ); const onInput = () => { - if ( fieldView.element != null ) { - let equationInput = fieldView.element.value.trim(); + const rawValue = mathInput.value ?? ''; + let equationInput = rawValue.trim(); - // If input has delimiters - if ( hasDelimiters( equationInput ) ) { - // Get equation without delimiters - const params = extractDelimiters( equationInput ); + // 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; + // Remove delimiters from input field + mathInput.value = params.equation; - equationInput = params.equation; + equationInput = params.equation; - // update display button and preview - this.displayButtonView.isOn = params.display; - } - if ( this.previewEnabled && this.mathView ) { - // Update preview view - this.mathView.value = equationInput; - } - - this.saveButtonView.isEnabled = !!equationInput; + // update display button and preview + this.displayButtonView.isOn = params.display; } + + // Sync to raw LaTeX input + this.rawLatexInputView.value = equationInput; + + if ( this.previewEnabled && this.mathView ) { + // Update preview view + this.mathView.value = equationInput; + } + + this.saveButtonView.isEnabled = !!equationInput; }; - fieldView.on( 'render', onInput ); - fieldView.on( 'input', onInput ); + mathInput.on( 'change:value', onInput ); return mathInput; } + /** + * Creates the raw LaTeX code textarea editor. + * + * Provides direct LaTeX 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 changes to MathLive visual editor + rawLatexInput.on( 'change:value', () => { + const rawValue = rawLatexInput.value ?? ''; + const equationInput = rawValue.trim(); + + // Update MathLive field + if ( this.mathInputView.value !== equationInput ) { + this.mathInputView.value = equationInput; + } + + // Update preview if enabled + if ( this.previewEnabled && this.mathView ) { + this.mathView.value = equationInput; + } + + this.saveButtonView.isEnabled = !!equationInput; + } ); + + return rawLatexInput; + } + private _createButton( label: string, icon: string, diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts new file mode 100644 index 000000000..a09ec641d --- /dev/null +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -0,0 +1,105 @@ +import { View, type Locale } from 'ckeditor5'; + +/** + * A view that wraps the MathLive `` web component for interactive LaTeX equation editing. + * + * MathLive provides a rich math input experience with live rendering, virtual keyboard support, + * and various accessibility features. + * + * @see https://cortexjs.io/mathlive/ + */ +export default class MathLiveInputView extends View { + /** + * The current LaTeX value of the math field. + * + * @observable + */ + public declare value: string; + + /** + * Whether the input is in read-only mode. + * + * @observable + */ + public declare isReadOnly: boolean; + + /** + * Reference to the `` DOM element. + */ + public mathfield: HTMLElement | null = null; + + constructor( locale: Locale ) { + super( locale ); + + this.set( 'value', '' ); + this.set( 'isReadOnly', false ); + + this.setTemplate( { + tag: 'div', + attributes: { + class: [ 'ck', 'ck-mathlive-input' ] + } + } ); + }git config --local credential.helper "" + + /** + * @inheritDoc + */ + public override render(): void { + super.render(); + + // Create the MathLive math-field custom element + const mathfield = document.createElement( 'math-field' ) as any; + this.mathfield = mathfield; + + // Configure the virtual keyboard to be manually controlled (shown by user interaction) + mathfield.setAttribute( 'virtual-keyboard-mode', 'manual' ); + + // Set initial value + if ( this.value ) { + ( mathfield as any ).value = this.value; + } + + // Bind readonly state + if ( this.isReadOnly ) { + ( mathfield as any ).readOnly = true; + } + + // Sync math-field changes to observable value + mathfield.addEventListener( 'input', () => { + this.value = ( mathfield as any ).value; + } ); + + // Sync observable value changes back to math-field + this.on( 'change:value', () => { + if ( ( mathfield as any ).value !== this.value ) { + ( mathfield as any ).value = this.value; + } + } ); + + // Sync readonly state to math-field + this.on( 'change:isReadOnly', () => { + ( mathfield as any ).readOnly = this.isReadOnly; + } ); + + this.element?.appendChild( mathfield ); + } + + /** + * Focuses the math-field element. + */ + public focus(): void { + this.mathfield?.focus(); + } + + /** + * @inheritDoc + */ + public override destroy(): void { + if ( this.mathfield ) { + this.mathfield.remove(); + this.mathfield = null; + } + super.destroy(); + } +} diff --git a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts new file mode 100644 index 000000000..871f23e72 --- /dev/null +++ b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts @@ -0,0 +1,61 @@ +import { LabeledFieldView, createLabeledTextarea, type Locale, type TextareaView } from 'ckeditor5'; + +/** + * A labeled textarea view for direct LaTeX code editing. + * + * This provides a plain text input for users who prefer to write LaTeX syntax directly + * or need to paste/edit raw LaTeX code. + */ +export default class RawLatexInputView extends LabeledFieldView { + /** + * 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; + + // Sync textarea input to observable value + fieldView.on( 'input', () => { + if ( fieldView.element ) { + this.value = fieldView.element.value; + } + } ); + + // Sync observable value changes back to textarea + this.on( 'change:value', () => { + if ( fieldView.element && fieldView.element.value !== this.value ) { + fieldView.element.value = this.value; + } + } ); + + // Sync readonly state (manual binding to avoid CKEditor observable rebind error) + this.on( 'change:isReadOnly', () => { + if ( fieldView.element ) { + fieldView.element.readOnly = this.isReadOnly; + } + } ); + } + + /** + * @inheritDoc + */ + public override render(): void { + super.render(); + // All styling is handled via CSS in mathform.css + } +} diff --git a/packages/ckeditor5-math/tests/mathui.ts b/packages/ckeditor5-math/tests/mathui.ts index 5a392c0db..a55fda523 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,10 @@ 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 hide the balloon on mainFormView#cancel if math command does not have a value', () => { diff --git a/packages/ckeditor5-math/theme/icons/cancel.svg b/packages/ckeditor5-math/theme/icons/cancel.svg new file mode 100644 index 000000000..6f755ce79 --- /dev/null +++ b/packages/ckeditor5-math/theme/icons/cancel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ckeditor5-math/theme/icons/check.svg b/packages/ckeditor5-math/theme/icons/check.svg new file mode 100644 index 000000000..d62f08d2e --- /dev/null +++ b/packages/ckeditor5-math/theme/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 3b7b4047f..45446d8f7 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -1,35 +1,227 @@ +/** + * Math equation editor dialog styles + * + * This stylesheet provides the layout and styling for the math equation editor, + * which includes a MathLive visual editor, a raw LaTeX textarea, and control buttons. + * The dialog is resizable and uses a flexible layout to accommodate different content sizes. + */ + +/* ============================================================================ + Form Layout + ========================================================================= */ + .ck.ck-math-form { display: flex; - align-items: flex-start; - flex-direction: row; - flex-wrap: nowrap; + flex-direction: column; padding: var(--ck-spacing-standard); + width: 100%; + height: 100%; + box-sizing: border-box; @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%; - } } } -.ck-math-tex.ck-placeholder::before { + +/* ============================================================================ + Button Row + ========================================================================= */ + +/* Button row */ +.ck-math-button-row { + display: flex; + gap: var(--ck-spacing-standard); + flex-shrink: 0; + margin-top: var(--ck-spacing-standard); + width: fit-content; +} + +/* Scrollable content area */ +.ck-math-scroll { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + min-width: 0; + overflow: hidden; +} + +/* ============================================================================ + Math Panel Layout + ========================================================================= */ + +/* Math panel layout */ +.ck-math-view { + display: flex; + flex-direction: column; + gap: var(--ck-spacing-standard); + flex: 1 1 auto; + min-height: fit-content; +} + +/* ============================================================================ + MathLive Integration + ========================================================================= */ + +/* MathLive input container */ +.ck.ck-mathlive-input { + display: inline-block; + flex: 0 0 auto; + width: auto; + min-width: 100%; + min-height: 140px; + max-height: 70vh; + resize: both; + overflow: auto; + padding-bottom: var(--ck-spacing-small); + box-sizing: border-box; +} + +/* MathLive field styling */ +.ck.ck-mathlive-input math-field { + display: block !important; + width: 100%; + height: 100%; + min-height: 140px; + box-sizing: border-box; + resize: none !important; + overflow: hidden !important; +} + +/* Style MathLive shadow DOM parts so the whole area is interactive */ +.ck.ck-math-form math-field::part(container), +.ck.ck-math-form math-field::part(content), +.ck.ck-math-form math-field::part(field) { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 100%; + height: 100%; + align-items: flex-start; + justify-content: flex-start; +} + + +/* Position MathLive virtual keyboard toggle button */ +.ck.ck-math-form math-field::part(virtual-keyboard-toggle) { + position: absolute; + right: 8px; + top: 8px; +} + +/* Position MathLive menu toggle button and ensure it's always visible */ +.ck.ck-math-form math-field::part(menu-toggle) { + position: absolute; + right: 8px; + top: 48px; + /* Force visibility even when field is empty */ + display: flex !important; + visibility: visible !important; +} + + +/* ============================================================================ + Raw LaTeX Integration + ========================================================================= */ + +/* Mirror MathLive container behavior for the labeled textarea wrapper */ + + +.ck-math-view .ck-labeled-field-view { + display: flex; + flex-direction: column; + flex: 0 0 auto; + width: 100%; + min-width: 100%; + min-height: 140px; + max-height: 70vh; + resize: vertical; + overflow: auto; + padding-bottom: 0; + box-sizing: border-box; + background: transparent; + border: none; + border-radius: 0; + outline: none; +} + +/* Let the internal wrapper stretch so the textarea can fill the space */ +.ck-math-view .ck-labeled-field-view .ck-labeled-field-view__input-wrapper { + display: flex; + flex-direction: column; + flex: 1 1 auto; + width: 100%; + height: auto; + min-height: 100px; + margin: 0; + padding: 0; + background: transparent; + border: none; + box-shadow: none; +} + +/* Ensure the raw textarea fills its wrapper like MathLive's math-field */ +.ck-math-view .ck-labeled-field-view textarea { + display: block; + width: 100% !important; + flex: 1 1 auto; + height: 100%; + min-height: 140px; + box-sizing: border-box; + resize: none !important; + overflow: hidden !important; +} + + +/* ============================================================================ + Shared Input Styling (MathLive & Raw LaTeX) + ========================================================================= */ + +/* Base styling for both MathLive field and raw LaTeX textarea */ +.ck.ck-math-form math-field, +.ck.ck-math-form textarea { + padding: var(--ck-spacing-small); + border: none !important; + border-radius: var(--ck-border-radius, 6px); + font-size: var(--ck-font-size-base); + box-sizing: border-box; + background: var(--input-background-color); + color: var(--input-text-color); + outline: 3px solid transparent; + outline-offset: 6px; +} + +/* Hover state */ +.ck.ck-math-form math-field:hover, +.ck.ck-math-form textarea:hover { + background: var(--input-hover-background); + color: var(--input-hover-color); +} + +/* Make the raw LaTeX textarea flat (no rounded corners or hover animation) */ +.ck-math-view .ck-labeled-field-view textarea { + border-radius: 0 !important; + outline: none !important; + box-shadow: none !important; + transition: none !important; +} + +.ck-math-view .ck-labeled-field-view textarea:hover, +.ck-math-view .ck-labeled-field-view textarea:focus { + background: var(--input-background-color); + color: var(--input-text-color); + outline: none !important; + box-shadow: none !important; + transition: none !important; +} + +/* Hide the internal label (we use a separate label element for better layout control) */ +.ck-math-view .ck-labeled-field-view .ck-label { display: none !important; } -.ck.ck-toolbar-container { - z-index: calc(var(--ck-z-panel) + 2); -} + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad305250f..c23634f4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -641,7 +641,7 @@ importers: version: 3.0.0 debug: specifier: 4.4.3 - version: 4.4.3(supports-color@6.0.0) + version: 4.4.3(supports-color@8.1.1) ejs: specifier: 3.1.10 version: 3.1.10 @@ -1058,6 +1058,9 @@ importers: '@ckeditor/ckeditor5-icons': specifier: 47.2.0 version: 47.2.0 + mathlive: + specifier: 0.108.2 + version: 0.108.2 devDependencies: '@ckeditor/ckeditor5-dev-build-tools': specifier: 43.1.0 @@ -2101,6 +2104,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'} @@ -6823,6 +6830,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==} @@ -10138,6 +10149,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==} @@ -15329,7 +15343,7 @@ snapshots: '@babel/traverse': 7.28.4 '@babel/types': 7.28.4 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -15446,7 +15460,7 @@ snapshots: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -15459,7 +15473,7 @@ snapshots: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -15471,7 +15485,7 @@ snapshots: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -15692,6 +15706,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@47.2.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -15756,8 +15772,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-watchdog': 47.2.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': dependencies: @@ -15952,8 +15966,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.2.0': dependencies: @@ -16866,6 +16878,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 @@ -16935,7 +16952,7 @@ snapshots: '@listr2/prompt-adapter-inquirer': 2.0.22(@inquirer/prompts@6.0.1) chalk: 4.1.2 commander: 11.1.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 10.1.0 listr2: 7.0.2 log-symbols: 4.1.0 @@ -16955,7 +16972,7 @@ snapshots: '@electron/rebuild': 3.7.2 '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) find-up: 5.0.0 fs-extra: 10.1.0 log-symbols: 4.1.0 @@ -16984,7 +17001,7 @@ snapshots: '@malept/cross-spawn-promise': 2.0.0 '@vscode/sudo-prompt': 9.3.1 chalk: 4.1.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fast-glob: 3.3.3 filenamify: 4.3.0 find-up: 5.0.0 @@ -17120,7 +17137,7 @@ snapshots: '@electron-forge/core-utils': 7.10.2 '@electron-forge/shared-types': 7.10.2 '@malept/cross-spawn-promise': 2.0.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 10.1.0 semver: 7.7.3 username: 5.1.0 @@ -17182,7 +17199,7 @@ snapshots: '@electron/get@2.0.3': dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) env-paths: 2.2.1 fs-extra: 8.1.0 got: 11.8.6 @@ -17196,7 +17213,7 @@ snapshots: '@electron/get@3.1.0': dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) env-paths: 2.2.1 fs-extra: 8.1.0 got: 11.8.6 @@ -17226,7 +17243,7 @@ snapshots: '@electron/notarize@2.5.0': dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 9.1.0 promise-retry: 2.0.1 transitivePeerDependencies: @@ -17235,7 +17252,7 @@ snapshots: '@electron/osx-sign@1.3.3': dependencies: compare-version: 0.1.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 10.1.0 isbinaryfile: 4.0.10 minimist: 1.2.8 @@ -17251,7 +17268,7 @@ snapshots: '@electron/osx-sign': 1.3.3 '@electron/universal': 2.0.2 '@electron/windows-sign': 1.2.1 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) extract-zip: 2.0.1 filenamify: 4.3.0 fs-extra: 11.3.2 @@ -17272,7 +17289,7 @@ snapshots: '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) detect-libc: 2.1.1 fs-extra: 10.1.0 got: 11.8.6 @@ -17291,7 +17308,7 @@ snapshots: dependencies: '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) detect-libc: 2.0.4 got: 11.8.6 graceful-fs: 4.2.11 @@ -17314,7 +17331,7 @@ snapshots: dependencies: '@electron/asar': 3.4.1 '@malept/cross-spawn-promise': 2.0.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) dir-compare: 4.2.0 fs-extra: 11.3.2 minimatch: 9.0.5 @@ -17325,7 +17342,7 @@ snapshots: '@electron/windows-sign@1.2.1': dependencies: cross-dirname: 0.1.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 11.3.2 minimist: 1.2.8 postject: 1.0.0-alpha.6 @@ -17612,7 +17629,7 @@ snapshots: '@eslint/config-array@0.21.1': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -17636,7 +17653,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -17996,7 +18013,7 @@ snapshots: '@antfu/install-pkg': 1.1.0 '@antfu/utils': 9.2.0 '@iconify/types': 2.0.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) globals: 15.15.0 kolorist: 1.8.0 local-pkg: 1.1.1 @@ -18414,7 +18431,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18498,7 +18515,7 @@ snapshots: '@malept/electron-installer-flatpak@0.11.4': dependencies: '@malept/flatpak-bundler': 0.4.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) electron-installer-common: 0.10.4 lodash: 4.17.21 semver: 7.7.3 @@ -18509,7 +18526,7 @@ snapshots: '@malept/flatpak-bundler@0.4.0': dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 9.1.0 lodash: 4.17.21 tmp-promise: 3.0.3 @@ -18968,7 +18985,7 @@ snapshots: '@puppeteer/browsers@2.10.10': dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 @@ -20249,7 +20266,7 @@ snapshots: '@tokenizer/inflate@0.2.7': dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fflate: 0.8.2 token-types: 6.0.0 transitivePeerDependencies: @@ -20866,7 +20883,7 @@ snapshots: '@typescript-eslint/types': 8.46.4 '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -20876,7 +20893,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) '@typescript-eslint/types': 8.46.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -20895,7 +20912,7 @@ snapshots: '@typescript-eslint/types': 8.46.4 '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 @@ -20910,7 +20927,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) '@typescript-eslint/types': 8.46.4 '@typescript-eslint/visitor-keys': 8.46.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -20992,7 +21009,7 @@ snapshots: '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4)': dependencies: '@istanbuljs/schema': 0.1.3 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 @@ -21010,7 +21027,7 @@ snapshots: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.3 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -21321,7 +21338,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -21765,7 +21782,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -22442,6 +22459,8 @@ snapshots: compare-versions@6.1.1: {} + complex-esm@2.1.1-esm1: {} + component-emitter@1.3.1: {} compress-commons@6.0.2: @@ -23225,8 +23244,7 @@ snapshots: decimal.js@10.5.0: {} - decimal.js@10.6.0: - optional: true + decimal.js@10.6.0: {} decko@1.2.0: {} @@ -23524,7 +23542,7 @@ snapshots: dependencies: '@electron/asar': 3.4.1 '@malept/cross-spawn-promise': 1.1.1 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 9.1.0 glob: 7.2.3 lodash: 4.17.21 @@ -23540,7 +23558,7 @@ snapshots: electron-installer-debian@3.2.0: dependencies: '@malept/cross-spawn-promise': 1.1.1 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) electron-installer-common: 0.10.4 fs-extra: 9.1.0 get-folder-size: 2.0.1 @@ -23554,7 +23572,7 @@ snapshots: electron-installer-dmg@5.0.1: dependencies: '@types/appdmg': 0.5.5 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) minimist: 1.2.8 optionalDependencies: appdmg: 0.6.6 @@ -23565,7 +23583,7 @@ snapshots: electron-installer-redhat@3.4.0: dependencies: '@malept/cross-spawn-promise': 1.1.1 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) electron-installer-common: 0.10.4 fs-extra: 9.1.0 lodash: 4.17.21 @@ -23581,7 +23599,7 @@ snapshots: electron-localshortcut@3.2.1: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) electron-is-accelerator: 0.1.2 keyboardevent-from-electron-accelerator: 2.0.0 keyboardevents-areequal: 0.2.2 @@ -23606,7 +23624,7 @@ snapshots: electron-winstaller@5.4.0: dependencies: '@electron/asar': 3.4.1 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 7.0.1 lodash: 4.17.21 temp: 0.9.4 @@ -24154,7 +24172,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -24264,7 +24282,7 @@ snapshots: express-http-proxy@2.1.2: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) es6-promise: 4.2.8 raw-body: 2.5.2 transitivePeerDependencies: @@ -24275,7 +24293,7 @@ snapshots: base64url: 3.0.1 clone: 2.1.2 cookie: 0.7.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) express: 5.1.0 futoin-hkdf: 1.5.3 http-errors: 1.8.1 @@ -24349,7 +24367,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -24398,7 +24416,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -24540,7 +24558,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -24596,7 +24614,7 @@ snapshots: flora-colossus@2.0.0: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 10.1.0 transitivePeerDependencies: - supports-color @@ -24608,7 +24626,7 @@ snapshots: follow-redirects@1.15.9(debug@4.4.3): optionalDependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) for-each@0.3.5: dependencies: @@ -24749,7 +24767,7 @@ snapshots: galactus@1.0.0: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) flora-colossus: 2.0.0 fs-extra: 10.1.0 transitivePeerDependencies: @@ -24865,7 +24883,7 @@ snapshots: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25342,7 +25360,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25350,14 +25368,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25410,14 +25428,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25812,7 +25830,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -26426,7 +26444,7 @@ snapshots: log4js@6.9.1: dependencies: date-format: 4.0.14 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) flatted: 3.3.3 rfdc: 1.4.1 streamroller: 3.1.5 @@ -26642,6 +26660,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: @@ -27019,7 +27041,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -27857,7 +27879,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) get-uri: 6.0.5 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -28133,7 +28155,7 @@ snapshots: portfinder@1.0.36: dependencies: async: 3.2.6 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -28978,7 +29000,7 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -29196,7 +29218,7 @@ snapshots: read-binary-file-arch@1.0.6: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -29639,7 +29661,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -29900,7 +29922,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -30117,13 +30139,13 @@ snapshots: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color simple-websocket@9.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) queue-microtask: 1.2.3 randombytes: 2.1.0 readable-stream: 3.6.2 @@ -30217,7 +30239,7 @@ snapshots: socks-proxy-agent@6.2.1: dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -30226,7 +30248,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -30234,7 +30256,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -30295,7 +30317,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -30306,7 +30328,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -30390,7 +30412,7 @@ snapshots: streamroller@3.1.5: dependencies: date-format: 4.0.14 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 8.1.0 transitivePeerDependencies: - supports-color @@ -30641,7 +30663,7 @@ snapshots: cosmiconfig: 9.0.0(typescript@5.0.4) css-functions-list: 3.2.3 css-tree: 3.1.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 file-entry-cache: 10.1.4 @@ -30685,7 +30707,7 @@ snapshots: cosmiconfig: 9.0.0(typescript@5.9.3) css-functions-list: 3.2.3 css-tree: 3.1.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 file-entry-cache: 10.1.4 @@ -30731,7 +30753,7 @@ snapshots: sumchecker@3.0.1: dependencies: - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -30739,7 +30761,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) fast-safe-stringify: 2.1.1 form-data: 4.0.4 formidable: 3.5.4 @@ -31196,7 +31218,7 @@ snapshots: tuf-js@4.0.0: dependencies: '@tufjs/models': 4.0.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) make-fetch-happen: 15.0.3 transitivePeerDependencies: - supports-color @@ -31566,7 +31588,7 @@ snapshots: vite-node@3.2.4(@types/node@24.10.1)(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.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.2.2(@types/node@24.10.1)(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.20.6)(yaml@2.8.1) @@ -31591,7 +31613,7 @@ snapshots: '@volar/typescript': 2.4.13 '@vue/language-core': 2.2.0(typescript@5.9.3) compare-versions: 6.1.1 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 1.1.1 magic-string: 0.30.17 @@ -31658,7 +31680,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) expect-type: 1.2.1 magic-string: 0.30.18 pathe: 2.0.3 @@ -31741,7 +31763,7 @@ snapshots: dependencies: chalk: 4.1.2 commander: 9.5.0 - debug: 4.4.3(supports-color@6.0.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color From e777b06fb8ae303e588eea793cade07d9c1884cd Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Thu, 20 Nov 2025 18:53:39 +0100 Subject: [PATCH 02/67] Math --- packages/ckeditor5-math/src/ui/mathliveinputview.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index a09ec641d..340314dc3 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -40,7 +40,7 @@ export default class MathLiveInputView extends View { class: [ 'ck', 'ck-mathlive-input' ] } } ); - }git config --local credential.helper "" + } /** * @inheritDoc From 49e90c08a9c2d6cbc3b8323a17aed2908f5fb722 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Thu, 20 Nov 2025 22:45:21 +0100 Subject: [PATCH 03/67] Better Names for Math UI Components --- packages/ckeditor5-math/package.json | 1 - packages/ckeditor5-math/src/mathui.ts | 21 +- .../ckeditor5-math/src/ui/mainformview.ts | 65 +++--- .../src/ui/mathliveinputview.ts | 17 +- ...nputView-value-to-math-command-value-1.png | Bin 0 -> 23437 bytes ...lement-when-math-command-is-disabled-1.png | Bin 0 -> 8642 bytes pnpm-lock.yaml | 203 ++++++++---------- 7 files changed, 149 insertions(+), 158 deletions(-) create mode 100644 packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-bind-mainFormView-mathInputView-value-to-math-command-value-1.png create mode 100644 packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---should-disable--mainFormView-element-when-math-command-is-disabled-1.png diff --git a/packages/ckeditor5-math/package.json b/packages/ckeditor5-math/package.json index 34f845f91..0f3e8a37a 100644 --- a/packages/ckeditor5-math/package.json +++ b/packages/ckeditor5-math/package.json @@ -71,7 +71,6 @@ ] }, "dependencies": { - "@ckeditor/ckeditor5-icons": "47.2.0", "mathlive": "0.108.2" } } diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index 2ac4c79cd..76290626f 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -57,7 +57,7 @@ export default class MathUI extends Plugin { this._balloon.showStack( 'main' ); requestAnimationFrame(() => { - this.formView?.mathInputView.focus(); + this.formView?.mathLiveInputView.focus(); }); } @@ -90,13 +90,22 @@ export default class MathUI extends Plugin { mathConfig.katexRenderOptions! ); - formView.mathInputView.bind( 'value' ).to( mathCommand, value => value ?? '' ); + formView.mathLiveInputView.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.mathLiveInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value ); + formView.saveButtonView.bind( 'isEnabled' ).to( + mathCommand, + 'isEnabled', + formView.mathLiveInputView, + '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', () => { @@ -151,7 +160,7 @@ export default class MathUI extends Plugin { } ); if ( this._balloon.visibleView === this.formView ) { - this.formView.mathInputView.focus(); + this.formView.mathLiveInputView.focus(); } // Show preview element diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index ef818e203..7c57fce8f 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -10,7 +10,7 @@ import type { KatexOptions } from '../typings-external.js'; export default class MainFormView extends View { public saveButtonView: ButtonView; - public mathInputView: MathLiveInputView; + public mathLiveInputView: MathLiveInputView; public rawLatexInputView: RawLatexInputView; public rawLatexLabel: LabelView; public displayButtonView: SwitchButtonView; @@ -46,8 +46,8 @@ export default class MainFormView extends View { this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null ); this.saveButtonView.type = 'submit'; - // Equation input - this.mathInputView = this._createMathInput(); + // MathLive visual equation editor + this.mathLiveInputView = this._createMathLiveInput(); // Raw LaTeX input this.rawLatexInputView = this._createRawLatexInput(); @@ -75,7 +75,7 @@ export default class MainFormView extends View { this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' ); children = [ - this.mathInputView, + this.mathLiveInputView, this.rawLatexLabel, this.rawLatexInputView, this.displayButtonView, @@ -84,7 +84,7 @@ export default class MainFormView extends View { ]; } else { children = [ - this.mathInputView, + this.mathLiveInputView, this.rawLatexLabel, this.rawLatexInputView, this.displayButtonView @@ -143,7 +143,7 @@ export default class MainFormView extends View { // Register form elements to focusable elements const childViews = [ - this.mathInputView, + this.mathLiveInputView, this.rawLatexInputView, this.displayButtonView, this.saveButtonView, @@ -168,14 +168,15 @@ export default class MainFormView extends View { } public get equation(): string { - return this.mathInputView.value ?? ''; + return this.mathLiveInputView.value ?? ''; } public set equation( equation: string ) { - this.mathInputView.value = equation; - this.rawLatexInputView.value = equation; + const normalizedEquation = equation.trim(); + this.mathLiveInputView.value = normalizedEquation.length ? normalizedEquation : null; + this.rawLatexInputView.value = normalizedEquation; if ( this.previewEnabled && this.mathView ) { - this.mathView.value = equation; + this.mathView.value = normalizedEquation; } } @@ -195,13 +196,13 @@ export default class MainFormView extends View { /** * Creates the MathLive visual equation editor. * - * Handles bidirectional synchronization with the raw LaTeX input and preview. + * Handles bidirectional synchronization with the raw LaTeX textarea and preview. */ - private _createMathInput() { - const mathInput = new MathLiveInputView( this.locale ); + private _createMathLiveInput() { + const mathLiveInput = new MathLiveInputView( this.locale ); const onInput = () => { - const rawValue = mathInput.value ?? ''; + const rawValue = mathLiveInput.value ?? ''; let equationInput = rawValue.trim(); // If input has delimiters @@ -210,56 +211,58 @@ export default class MainFormView extends View { const params = extractDelimiters( equationInput ); // Remove delimiters from input field - mathInput.value = params.equation; + mathLiveInput.value = params.equation; equationInput = params.equation; - // update display button and preview + // Update display button and preview this.displayButtonView.isOn = params.display; } - // Sync to raw LaTeX input + 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 view + // Update preview this.mathView.value = equationInput; } - - this.saveButtonView.isEnabled = !!equationInput; }; - mathInput.on( 'change:value', onInput ); + mathLiveInput.on( 'change:value', onInput ); - return mathInput; + return mathLiveInput; } /** - * Creates the raw LaTeX code textarea editor. + * Creates the raw LaTeX textarea editor. * - * Provides direct LaTeX editing and synchronizes changes with the MathLive visual 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 changes to MathLive visual editor + // Sync raw LaTeX textarea changes to MathLive visual editor rawLatexInput.on( 'change:value', () => { const rawValue = rawLatexInput.value ?? ''; const equationInput = rawValue.trim(); - // Update MathLive field - if ( this.mathInputView.value !== equationInput ) { - this.mathInputView.value = equationInput; + // Update MathLive visual editor + const normalizedEquation = equationInput.length ? equationInput : null; + if ( this.mathLiveInputView.value !== normalizedEquation ) { + this.mathLiveInputView.value = normalizedEquation; } - // Update preview if enabled + // Update preview if ( this.previewEnabled && this.mathView ) { this.mathView.value = equationInput; } - - this.saveButtonView.isEnabled = !!equationInput; } ); return rawLatexInput; diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index 340314dc3..148ae7f14 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -14,7 +14,7 @@ export default class MathLiveInputView extends View { * * @observable */ - public declare value: string; + public declare value: string | null; /** * Whether the input is in read-only mode. @@ -31,7 +31,7 @@ export default class MathLiveInputView extends View { constructor( locale: Locale ) { super( locale ); - this.set( 'value', '' ); + this.set( 'value', null ); this.set( 'isReadOnly', false ); this.setTemplate( { @@ -56,8 +56,9 @@ export default class MathLiveInputView extends View { mathfield.setAttribute( 'virtual-keyboard-mode', 'manual' ); // Set initial value - if ( this.value ) { - ( mathfield as any ).value = this.value; + const initialValue = this.value ?? ''; + if ( initialValue ) { + ( mathfield as any ).value = initialValue; } // Bind readonly state @@ -67,13 +68,15 @@ export default class MathLiveInputView extends View { // Sync math-field changes to observable value mathfield.addEventListener( 'input', () => { - this.value = ( mathfield as any ).value; + const nextValue: string = ( mathfield as any ).value; + this.value = nextValue.length ? nextValue : null; } ); // Sync observable value changes back to math-field this.on( 'change:value', () => { - if ( ( mathfield as any ).value !== this.value ) { - ( mathfield as any ).value = this.value; + const nextValue = this.value ?? ''; + if ( ( mathfield as any ).value !== nextValue ) { + ( mathfield as any ).value = nextValue; } } ); diff --git a/packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-bind-mainFormView-mathInputView-value-to-math-command-value-1.png b/packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-bind-mainFormView-mathInputView-value-to-math-command-value-1.png new file mode 100644 index 0000000000000000000000000000000000000000..30a255f4a953e913a0e192c9873ece104bc7f4bd GIT binary patch literal 23437 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 UOQ2L9t{`rxYF*90a_ixL0i4=(egFUf literal 0 HcmV?d00001 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aacb56ee4..903192f4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -641,7 +641,7 @@ importers: version: 3.0.0 debug: specifier: 4.4.3 - version: 4.4.3(supports-color@8.1.1) + version: 4.4.3(supports-color@6.0.0) ejs: specifier: 3.1.10 version: 3.1.10 @@ -15356,7 +15356,7 @@ snapshots: '@babel/traverse': 7.28.4 '@babel/types': 7.28.4 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -15479,7 +15479,7 @@ snapshots: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -15492,7 +15492,7 @@ snapshots: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -15504,7 +15504,7 @@ snapshots: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -16498,8 +16498,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-restricted-editing@47.2.0': dependencies: @@ -16978,7 +16976,7 @@ snapshots: '@listr2/prompt-adapter-inquirer': 2.0.22(@inquirer/prompts@6.0.1) chalk: 4.1.2 commander: 11.1.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 10.1.0 listr2: 7.0.2 log-symbols: 4.1.0 @@ -16998,7 +16996,7 @@ snapshots: '@electron/rebuild': 3.7.2 '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) find-up: 5.0.0 fs-extra: 10.1.0 log-symbols: 4.1.0 @@ -17027,7 +17025,7 @@ snapshots: '@malept/cross-spawn-promise': 2.0.0 '@vscode/sudo-prompt': 9.3.1 chalk: 4.1.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fast-glob: 3.3.3 filenamify: 4.3.0 find-up: 5.0.0 @@ -17163,7 +17161,7 @@ snapshots: '@electron-forge/core-utils': 7.10.2 '@electron-forge/shared-types': 7.10.2 '@malept/cross-spawn-promise': 2.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 10.1.0 semver: 7.7.3 username: 5.1.0 @@ -17225,7 +17223,7 @@ snapshots: '@electron/get@2.0.3': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) env-paths: 2.2.1 fs-extra: 8.1.0 got: 11.8.6 @@ -17239,7 +17237,7 @@ snapshots: '@electron/get@3.1.0': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) env-paths: 2.2.1 fs-extra: 8.1.0 got: 11.8.6 @@ -17269,7 +17267,7 @@ snapshots: '@electron/notarize@2.5.0': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 9.1.0 promise-retry: 2.0.1 transitivePeerDependencies: @@ -17278,7 +17276,7 @@ snapshots: '@electron/osx-sign@1.3.3': dependencies: compare-version: 0.1.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 10.1.0 isbinaryfile: 4.0.10 minimist: 1.2.8 @@ -17294,7 +17292,7 @@ snapshots: '@electron/osx-sign': 1.3.3 '@electron/universal': 2.0.2 '@electron/windows-sign': 1.2.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) extract-zip: 2.0.1 filenamify: 4.3.0 fs-extra: 11.3.2 @@ -17315,7 +17313,7 @@ snapshots: '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) detect-libc: 2.1.1 fs-extra: 10.1.0 got: 11.8.6 @@ -17334,7 +17332,7 @@ snapshots: dependencies: '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) detect-libc: 2.0.4 got: 11.8.6 graceful-fs: 4.2.11 @@ -17357,7 +17355,7 @@ snapshots: dependencies: '@electron/asar': 3.4.1 '@malept/cross-spawn-promise': 2.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) dir-compare: 4.2.0 fs-extra: 11.3.2 minimatch: 9.0.5 @@ -17368,7 +17366,7 @@ snapshots: '@electron/windows-sign@1.2.1': dependencies: cross-dirname: 0.1.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 11.3.2 minimist: 1.2.8 postject: 1.0.0-alpha.6 @@ -17655,7 +17653,7 @@ snapshots: '@eslint/config-array@0.21.1': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -17679,7 +17677,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -18039,7 +18037,7 @@ snapshots: '@antfu/install-pkg': 1.1.0 '@antfu/utils': 9.2.0 '@iconify/types': 2.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) globals: 15.15.0 kolorist: 1.8.0 local-pkg: 1.1.1 @@ -18457,7 +18455,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -18541,7 +18539,7 @@ snapshots: '@malept/electron-installer-flatpak@0.11.4': dependencies: '@malept/flatpak-bundler': 0.4.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) electron-installer-common: 0.10.4 lodash: 4.17.21 semver: 7.7.3 @@ -18552,7 +18550,7 @@ snapshots: '@malept/flatpak-bundler@0.4.0': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 9.1.0 lodash: 4.17.21 tmp-promise: 3.0.3 @@ -19011,7 +19009,7 @@ snapshots: '@puppeteer/browsers@2.10.10': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 @@ -20279,7 +20277,7 @@ snapshots: '@tokenizer/inflate@0.2.7': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fflate: 0.8.2 token-types: 6.0.0 transitivePeerDependencies: @@ -20905,7 +20903,7 @@ snapshots: '@typescript-eslint/types': 8.46.4 '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.46.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -20927,7 +20925,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) '@typescript-eslint/types': 8.46.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -20964,7 +20962,7 @@ snapshots: '@typescript-eslint/types': 8.46.4 '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 @@ -20993,7 +20991,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) '@typescript-eslint/types': 8.46.4 '@typescript-eslint/visitor-keys': 8.46.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -21115,7 +21113,7 @@ snapshots: '@vitest/coverage-istanbul@4.0.10(vitest@4.0.10)': dependencies: '@istanbuljs/schema': 0.1.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 @@ -21130,7 +21128,8 @@ snapshots: '@vitest/coverage-v8@4.0.10(@vitest/browser@4.0.10(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.1)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.2.2(@types/node@24.10.1)(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.20.6)(yaml@2.8.1))(vitest@4.0.10))(vitest@4.0.10)': dependencies: '@bcoe/v8-coverage': 1.0.2 - ast-v8-to-istanbul: 0.3.3 + '@vitest/utils': 4.0.10 + ast-v8-to-istanbul: 0.3.8 debug: 4.4.3(supports-color@6.0.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -21436,7 +21435,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -21872,7 +21871,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -23618,7 +23617,7 @@ snapshots: dependencies: '@electron/asar': 3.4.1 '@malept/cross-spawn-promise': 1.1.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 9.1.0 glob: 7.2.3 lodash: 4.17.21 @@ -23634,7 +23633,7 @@ snapshots: electron-installer-debian@3.2.0: dependencies: '@malept/cross-spawn-promise': 1.1.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) electron-installer-common: 0.10.4 fs-extra: 9.1.0 get-folder-size: 2.0.1 @@ -23648,7 +23647,7 @@ snapshots: electron-installer-dmg@5.0.1: dependencies: '@types/appdmg': 0.5.5 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) minimist: 1.2.8 optionalDependencies: appdmg: 0.6.6 @@ -23659,7 +23658,7 @@ snapshots: electron-installer-redhat@3.4.0: dependencies: '@malept/cross-spawn-promise': 1.1.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) electron-installer-common: 0.10.4 fs-extra: 9.1.0 lodash: 4.17.21 @@ -23675,7 +23674,7 @@ snapshots: electron-localshortcut@3.2.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) electron-is-accelerator: 0.1.2 keyboardevent-from-electron-accelerator: 2.0.0 keyboardevents-areequal: 0.2.2 @@ -23700,7 +23699,7 @@ snapshots: electron-winstaller@5.4.0: dependencies: '@electron/asar': 3.4.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 7.0.1 lodash: 4.17.21 temp: 0.9.4 @@ -24248,7 +24247,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -24358,7 +24357,7 @@ snapshots: express-http-proxy@2.1.2: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) es6-promise: 4.2.8 raw-body: 2.5.2 transitivePeerDependencies: @@ -24369,7 +24368,7 @@ snapshots: base64url: 3.0.1 clone: 2.1.2 cookie: 0.7.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) express: 5.1.0 futoin-hkdf: 1.5.3 http-errors: 1.8.1 @@ -24443,7 +24442,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -24492,7 +24491,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -24634,7 +24633,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -24690,7 +24689,7 @@ snapshots: flora-colossus@2.0.0: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 10.1.0 transitivePeerDependencies: - supports-color @@ -24702,7 +24701,7 @@ snapshots: follow-redirects@1.15.9(debug@4.4.3): optionalDependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) for-each@0.3.5: dependencies: @@ -24843,7 +24842,7 @@ snapshots: galactus@1.0.0: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) flora-colossus: 2.0.0 fs-extra: 10.1.0 transitivePeerDependencies: @@ -24959,7 +24958,7 @@ snapshots: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -25436,7 +25435,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -25444,14 +25443,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -25504,14 +25503,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -25906,7 +25905,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -26520,7 +26519,7 @@ snapshots: log4js@6.9.1: dependencies: date-format: 4.0.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) flatted: 3.3.3 rfdc: 1.4.1 streamroller: 3.1.5 @@ -27119,7 +27118,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -27957,7 +27956,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) get-uri: 6.0.5 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -28235,7 +28234,7 @@ snapshots: portfinder@1.0.36: dependencies: async: 3.2.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -29074,7 +29073,7 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -29290,7 +29289,7 @@ snapshots: read-binary-file-arch@1.0.6: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -29733,7 +29732,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -29994,7 +29993,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -30211,13 +30210,13 @@ snapshots: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color simple-websocket@9.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) queue-microtask: 1.2.3 randombytes: 2.1.0 readable-stream: 3.6.2 @@ -30311,7 +30310,7 @@ snapshots: socks-proxy-agent@6.2.1: dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -30320,7 +30319,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -30328,7 +30327,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -30389,7 +30388,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -30400,7 +30399,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -30484,7 +30483,7 @@ snapshots: streamroller@3.1.5: dependencies: date-format: 4.0.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fs-extra: 8.1.0 transitivePeerDependencies: - supports-color @@ -30731,7 +30730,7 @@ snapshots: cosmiconfig: 9.0.0(typescript@5.0.4) css-functions-list: 3.2.3 css-tree: 3.1.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 file-entry-cache: 10.1.4 @@ -30775,7 +30774,7 @@ snapshots: cosmiconfig: 9.0.0(typescript@5.9.3) css-functions-list: 3.2.3 css-tree: 3.1.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 file-entry-cache: 10.1.4 @@ -30821,7 +30820,7 @@ snapshots: sumchecker@3.0.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color @@ -30829,7 +30828,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) fast-safe-stringify: 2.1.1 form-data: 4.0.4 formidable: 3.5.4 @@ -31276,7 +31275,7 @@ snapshots: tuf-js@4.0.0: dependencies: '@tufjs/models': 4.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) make-fetch-happen: 15.0.3 transitivePeerDependencies: - supports-color @@ -31654,27 +31653,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.2.4(@types/node@24.10.1)(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.20.6)(yaml@2.8.1): - dependencies: - cac: 6.7.14 - debug: 4.4.3(supports-color@6.0.0) - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.2.2(@types/node@24.10.1)(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.20.6)(yaml@2.8.1) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite-plugin-dts@4.5.4(@types/node@24.10.1)(rollup@4.52.0)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(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.20.6)(yaml@2.8.1)): dependencies: '@microsoft/api-extractor': 7.52.8(@types/node@24.10.1) @@ -31682,7 +31660,7 @@ snapshots: '@volar/typescript': 2.4.13 '@vue/language-core': 2.2.0(typescript@5.9.3) compare-versions: 6.1.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) kolorist: 1.8.0 local-pkg: 1.1.1 magic-string: 0.30.21 @@ -31740,18 +31718,17 @@ snapshots: vitest@4.0.10(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser-webdriverio@4.0.10)(@vitest/ui@4.0.10)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.30.1)(msw@2.7.5(@types/node@24.10.1)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.7.5(@types/node@24.10.1)(typescript@5.9.3))(vite@7.2.2(@types/node@24.10.1)(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.20.6)(yaml@2.8.1)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.0 + '@vitest/expect': 4.0.10 + '@vitest/mocker': 4.0.10(msw@2.7.5(@types/node@24.10.1)(typescript@5.9.3))(vite@7.2.2(@types/node@24.10.1)(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.20.6)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.10 + '@vitest/runner': 4.0.10 + '@vitest/snapshot': 4.0.10 + '@vitest/spy': 4.0.10 + '@vitest/utils': 4.0.10 debug: 4.4.3(supports-color@6.0.0) - expect-type: 1.2.1 - magic-string: 0.30.18 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 pathe: 2.0.3 picomatch: 4.0.3 std-env: 3.10.0 @@ -31830,7 +31807,7 @@ snapshots: dependencies: chalk: 4.1.2 commander: 9.5.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@6.0.0) transitivePeerDependencies: - supports-color From 39838c25c29b113a17e5293dd09ab7ab9aa3aa0d Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Fri, 21 Nov 2025 23:50:49 +0100 Subject: [PATCH 04/67] Fixed chaching problems --- apps/client/package.json | 1 + apps/client/vite.config.mts | 2 +- packages/ckeditor5-math/src/index.ts | 1 + packages/ckeditor5-math/src/mathui.ts | 1 - packages/ckeditor5-math/src/ui/mathliveinputview.ts | 1 + pnpm-lock.yaml | 10 +++++++--- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index ba0fbb97b..d8db8801d 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -54,6 +54,7 @@ "leaflet-gpx": "2.2.0", "mark.js": "8.11.1", "marked": "17.0.0", + "mathlive": "0.108.2", "mermaid": "11.12.1", "mind-elixir": "5.3.6", "normalize.css": "8.0.1", diff --git a/apps/client/vite.config.mts b/apps/client/vite.config.mts index 8d3b0c583..12eac25ce 100644 --- a/apps/client/vite.config.mts +++ b/apps/client/vite.config.mts @@ -83,7 +83,7 @@ export default defineConfig(() => ({ chunkFileNames: "src/[name].js", assetFileNames: "src/[name].[ext]", manualChunks: { - "ckeditor5": [ "@triliumnext/ckeditor5" ], + "ckeditor5": [ "@triliumnext/ckeditor5", "mathlive" ], "boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ] }, }, diff --git a/packages/ckeditor5-math/src/index.ts b/packages/ckeditor5-math/src/index.ts index b3475309f..eac4d7b22 100644 --- a/packages/ckeditor5-math/src/index.ts +++ b/packages/ckeditor5-math/src/index.ts @@ -1,6 +1,7 @@ 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'; diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index 76290626f..504adf77a 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -4,7 +4,6 @@ import mathIcon from '../theme/icons/math.svg?raw'; import { Plugin, ClickObserver, ButtonView, ContextualBalloon, clickOutsideHandler, CKEditorError, uid } from 'ckeditor5'; import { getBalloonPositionData } from './utils.js'; import MathCommand from './mathcommand.js'; -import 'mathlive'; const mathKeystroke = 'Ctrl+M'; diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index 148ae7f14..689ec891a 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -1,4 +1,5 @@ import { View, type Locale } from 'ckeditor5'; +import 'mathlive'; /** * A view that wraps the MathLive `` web component for interactive LaTeX equation editing. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 903192f4f..1e470a489 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -274,6 +274,9 @@ importers: marked: specifier: 17.0.0 version: 17.0.0 + mathlive: + specifier: 0.108.2 + version: 0.108.2 mermaid: specifier: 11.12.1 version: 11.12.1 @@ -1055,9 +1058,6 @@ importers: packages/ckeditor5-math: dependencies: - '@ckeditor/ckeditor5-icons': - specifier: 47.2.0 - version: 47.2.0 mathlive: specifier: 0.108.2 version: 0.108.2 @@ -15584,6 +15584,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.2.0 '@ckeditor/ckeditor5-upload': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-ai@47.2.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: @@ -15990,6 +15992,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.2.0': dependencies: From 569b09609da96bba97eac7d47e39b488e95e8395 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sat, 22 Nov 2025 00:01:14 +0100 Subject: [PATCH 05/67] Remove mathlive dependency and chunking --- apps/client/package.json | 1 - apps/client/vite.config.mts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index d8db8801d..ba0fbb97b 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -54,7 +54,6 @@ "leaflet-gpx": "2.2.0", "mark.js": "8.11.1", "marked": "17.0.0", - "mathlive": "0.108.2", "mermaid": "11.12.1", "mind-elixir": "5.3.6", "normalize.css": "8.0.1", diff --git a/apps/client/vite.config.mts b/apps/client/vite.config.mts index 12eac25ce..8d3b0c583 100644 --- a/apps/client/vite.config.mts +++ b/apps/client/vite.config.mts @@ -83,7 +83,7 @@ export default defineConfig(() => ({ chunkFileNames: "src/[name].js", assetFileNames: "src/[name].[ext]", manualChunks: { - "ckeditor5": [ "@triliumnext/ckeditor5", "mathlive" ], + "ckeditor5": [ "@triliumnext/ckeditor5" ], "boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ] }, }, From 4eef30f8b5a093f5011f946b7da7baaa311415ed Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sat, 22 Nov 2025 00:20:20 +0100 Subject: [PATCH 06/67] Fix names --- ...command-on-mainFormView-submit-event-1.png | Bin 0 -> 23437 bytes packages/ckeditor5-math/tests/mathui.ts | 20 ++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-execute-math-command-on-mainFormView-submit-event-1.png diff --git a/packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-execute-math-command-on-mainFormView-submit-event-1.png b/packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-execute-math-command-on-mainFormView-submit-event-1.png new file mode 100644 index 0000000000000000000000000000000000000000..30a255f4a953e913a0e192c9873ece104bc7f4bd GIT binary patch literal 23437 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 { command.isEnabled = true; - expect( formView!.mathInputView.isReadOnly ).to.be.false; + expect( formView!.mathLiveInputView.isReadOnly ).to.be.false; expect( formView!.saveButtonView.isEnabled ).to.be.false; expect( formView!.cancelButtonView.isEnabled ).to.be.true; command.isEnabled = false; - expect( formView!.mathInputView.isReadOnly ).to.be.true; + expect( formView!.mathLiveInputView.isReadOnly ).to.be.true; expect( formView!.saveButtonView.isEnabled ).to.be.false; expect( formView!.cancelButtonView.isEnabled ).to.be.true; } ); @@ -407,24 +407,32 @@ describe( 'MathUI', () => { setModelData( editor.model, 'f[o]o' ); } ); - it( 'should bind mainFormView.mathInputView#value to math command value', () => { + it( 'should bind mainFormView.mathLiveInputView#value to math command value', () => { const command = editor.commands.get( 'math' ); - expect( formView!.mathInputView.value ).to.be.null; + expect( formView!.mathLiveInputView.value ).to.be.null; command!.value = 'x^2'; - expect( formView!.mathInputView.value ).to.equal( 'x^2' ); + expect( formView!.mathLiveInputView.value ).to.equal( 'x^2' ); } ); it( 'should execute math command on mainFormView#submit event', () => { const executeSpy = vi.spyOn( editor, 'execute' ); - formView!.mathInputView.value = 'x^2'; + formView!.mathLiveInputView.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' ); + + formView!.rawLatexInputView.value = '\\frac{1}{2}'; + expect( formView!.mathLiveInputView.value ).to.equal( '\\frac{1}{2}' ); + } ); + it( 'should hide the balloon on mainFormView#cancel if math command does not have a value', () => { mathUIFeature._showUI(); formView!.fire( 'cancel' ); From e225794f72da5c8912d82ea6614c077daee531d8 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sat, 22 Nov 2025 21:35:37 +0100 Subject: [PATCH 07/67] Better window focus handling in MathFormView --- .../ckeditor5-math/src/ui/mainformview.ts | 80 ++++++++++++++++++- packages/ckeditor5-math/theme/mathform.css | 2 +- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 7c57fce8f..f5f630dff 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -19,7 +19,6 @@ export default class MainFormView extends View { public previewLabel?: LabelView; public mathView?: MathView; public override locale: Locale = new Locale(); - public lazyLoad: undefined | ( () => Promise ); constructor( locale: Locale, @@ -161,6 +160,16 @@ export default class MainFormView extends View { 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 { @@ -193,6 +202,75 @@ export default class MainFormView extends View { } } ); + 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. * diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 45446d8f7..5821c7fd2 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -136,7 +136,7 @@ min-width: 100%; min-height: 140px; max-height: 70vh; - resize: vertical; + resize: both; overflow: auto; padding-bottom: 0; box-sizing: border-box; From 48a4b81fbef61e131ab739b90d55797b249111c7 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sat, 22 Nov 2025 21:40:55 +0100 Subject: [PATCH 08/67] remove automated screenshot files --- .gitignore | 2 +- ...hInputView-value-to-math-command-value-1.png | Bin 23437 -> 0 bytes ...h-command-on-mainFormView-submit-event-1.png | Bin 23437 -> 0 bytes ...-element-when-math-command-is-disabled-1.png | Bin 8642 -> 0 bytes 4 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-bind-mainFormView-mathInputView-value-to-math-command-value-1.png delete mode 100644 packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-execute-math-command-on-mainFormView-submit-event-1.png delete mode 100644 packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---should-disable--mainFormView-element-when-math-command-is-disabled-1.png diff --git a/.gitignore b/.gitignore index b2c4e3c46..9321d866f 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,4 @@ upload .svelte-kit # docs -site/ +**/__screenshots__/ diff --git a/packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-bind-mainFormView-mathInputView-value-to-math-command-value-1.png b/packages/ckeditor5-math/tests/__screenshots__/mathui.ts/MathUI--showUI---math-form-view-binding-should-bind-mainFormView-mathInputView-value-to-math-command-value-1.png deleted file mode 100644 index 30a255f4a953e913a0e192c9873ece104bc7f4bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23437 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 UOQ2L9t{`rxYF*90a_ixL0i4=(egFUf From de80eb4806e17f6e04b843c21fdd6cdc5672475a Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sat, 22 Nov 2025 22:42:34 +0100 Subject: [PATCH 09/67] Improve mathform.css styling for better visual integration --- packages/ckeditor5-math/theme/mathform.css | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 5821c7fd2..6a15c852a 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -186,19 +186,12 @@ border-radius: var(--ck-border-radius, 6px); font-size: var(--ck-font-size-base); box-sizing: border-box; - background: var(--input-background-color); - color: var(--input-text-color); + background: var(--ck-color-input-background,) !important; + color: var(--ck-color-input-text, inherit); outline: 3px solid transparent; outline-offset: 6px; } -/* Hover state */ -.ck.ck-math-form math-field:hover, -.ck.ck-math-form textarea:hover { - background: var(--input-hover-background); - color: var(--input-hover-color); -} - /* Make the raw LaTeX textarea flat (no rounded corners or hover animation) */ .ck-math-view .ck-labeled-field-view textarea { border-radius: 0 !important; @@ -209,8 +202,8 @@ .ck-math-view .ck-labeled-field-view textarea:hover, .ck-math-view .ck-labeled-field-view textarea:focus { - background: var(--input-background-color); - color: var(--input-text-color); + background: var(--ck-color-input-background, ) !important; + color: var(--ck-color-input-text, inherit) !important; outline: none !important; box-shadow: none !important; transition: none !important; From a0f16f918428edbbee4bcd5830ac09af04db4cdd Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sun, 23 Nov 2025 13:09:56 +0100 Subject: [PATCH 10/67] Fix typos in mathform.css --- packages/ckeditor5-math/theme/mathform.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 6a15c852a..9537cdbb0 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -186,7 +186,7 @@ border-radius: var(--ck-border-radius, 6px); font-size: var(--ck-font-size-base); box-sizing: border-box; - background: var(--ck-color-input-background,) !important; + background: var(--ck-color-input-background) !important; color: var(--ck-color-input-text, inherit); outline: 3px solid transparent; outline-offset: 6px; @@ -202,7 +202,7 @@ .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; + background: var(--ck-color-input-background) !important; color: var(--ck-color-input-text, inherit) !important; outline: none !important; box-shadow: none !important; From 56834cb88a44c2d4d55188423141fecd8fe8b672 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sun, 23 Nov 2025 13:29:26 +0100 Subject: [PATCH 11/67] Improve MathLive and Raw LaTeX input views to propagate mousedown events --- .../ckeditor5-math/src/ui/mainformview.ts | 25 +++++++++++-------- .../src/ui/mathliveinputview.ts | 5 ++++ .../src/ui/rawlatexinputview.ts | 13 +++++++--- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index f5f630dff..3a1d913d8 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -168,8 +168,6 @@ export default class MainFormView extends View { 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 { @@ -212,8 +210,7 @@ export default class MainFormView extends View { if ( this.rawLatexInputView.element ) this._resizeObserver?.observe( this.rawLatexInputView.element ); }; - private _onMouseDown = ( evt: Event ) => { - const target = evt.currentTarget as HTMLElement; + private _onMouseDown( target: HTMLElement ) { this._activeResizeTarget = target; // Stop observing the OTHER element to prevent loops and errors while resizing @@ -226,15 +223,21 @@ export default class MainFormView extends View { 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 ); - } + this.listenTo( this.mathLiveInputView, 'mousedown', () => { + if ( this.mathLiveInputView.element ) { + this._onMouseDown( this.mathLiveInputView.element ); + } + } ); + + this.listenTo( this.rawLatexInputView, 'mousedown', () => { + if ( this.rawLatexInputView.element ) { + this._onMouseDown( this.rawLatexInputView.element ); + } + } ); + document.addEventListener( 'mouseup', this._onMouseUp ); // Synchronize width between MathLive and Raw LaTeX inputs diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index 689ec891a..9dbce2903 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -49,6 +49,11 @@ export default class MathLiveInputView extends View { public override render(): void { super.render(); + // Propagate mousedown event to the view + this.element!.addEventListener( 'mousedown', ( evt ) => { + this.fire( 'mousedown', evt ); + } ); + // Create the MathLive math-field custom element const mathfield = document.createElement( 'math-field' ) as any; this.mathfield = mathfield; diff --git a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts index 871f23e72..4c994778d 100644 --- a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts +++ b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts @@ -2,21 +2,21 @@ import { LabeledFieldView, createLabeledTextarea, type Locale, type TextareaView /** * A labeled textarea view for direct LaTeX code editing. - * + * * This provides a plain text input for users who prefer to write LaTeX syntax directly * or need to paste/edit raw LaTeX code. */ export default class RawLatexInputView extends LabeledFieldView { /** * The current LaTeX value. - * + * * @observable */ public declare value: string; - + /** * Whether the input is in read-only mode. - * + * * @observable */ public declare isReadOnly: boolean; @@ -57,5 +57,10 @@ export default class RawLatexInputView extends LabeledFieldView { public override render(): void { super.render(); // All styling is handled via CSS in mathform.css + + // Propagate mousedown event to the view + this.element!.addEventListener( 'mousedown', ( evt ) => { + this.fire( 'mousedown', evt ); + } ); } } From 1471a726337ac5ac1b017726f5a6db2d46b1c89a Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sun, 23 Nov 2025 13:34:22 +0100 Subject: [PATCH 12/67] refactor: avoid recursive updates in mathLiveInput by normalizing value before updateing --- .../ckeditor5-math/src/ui/mainformview.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 3a1d913d8..46f6e1aaf 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -283,33 +283,29 @@ export default class MainFormView extends View { const mathLiveInput = new MathLiveInputView( this.locale ); const onInput = () => { - const rawValue = mathLiveInput.value ?? ''; - let equationInput = rawValue.trim(); + let equationInput = ( mathLiveInput.value ?? '' ).trim(); - // If input has delimiters + // If input has delimiters, strip them and update the display mode. 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; + + // Update self if needed. if ( mathLiveInput.value !== normalizedEquation ) { mathLiveInput.value = normalizedEquation; } - // Sync to raw LaTeX textarea - this.rawLatexInputView.value = equationInput; + // Sync to raw LaTeX textarea if its value is different. + if ( this.rawLatexInputView.value !== equationInput ) { + this.rawLatexInputView.value = equationInput; + } - if ( this.previewEnabled && this.mathView ) { - // Update preview + // Update preview if enabled and its value is different. + if ( this.previewEnabled && this.mathView && this.mathView.value !== equationInput ) { this.mathView.value = equationInput; } }; From edba8188fef402b5e1cf3f38c9fd5231d266b923 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sun, 23 Nov 2025 13:44:28 +0100 Subject: [PATCH 13/67] Fix dark selection colors in MathLive math-field --- packages/ckeditor5-math/theme/mathform.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 9537cdbb0..300e759d3 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -192,6 +192,13 @@ outline-offset: 6px; } +/* Override MathLive selection colors to prevent dark blocks in light mode */ +.ck.ck-math-form math-field { + --selection-background-color: rgba(33, 150, 243, 0.2); + --selection-color: inherit; + --contains-highlight-background-color: rgba(0, 0, 0, 0.05); +} + /* Make the raw LaTeX textarea flat (no rounded corners or hover animation) */ .ck-math-view .ck-labeled-field-view textarea { border-radius: 0 !important; From 5821c350e18e5d68da39b92ad1d9f78c0cde5911 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sun, 23 Nov 2025 17:58:51 +0100 Subject: [PATCH 14/67] Fixing class property initialization order --- .../ckeditor5-math/src/ui/mainformview.ts | 21 ++++++++++--------- pnpm-lock.yaml | 9 ++------ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 46f6e1aaf..ac25bc5f0 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -9,16 +9,16 @@ 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(); + public declare saveButtonView: ButtonView; + public declare mathLiveInputView: MathLiveInputView; + public declare rawLatexInputView: RawLatexInputView; + public declare rawLatexLabel: LabelView; + public declare displayButtonView: SwitchButtonView; + public declare cancelButtonView: ButtonView; + public declare previewEnabled: boolean; + public declare previewLabel?: LabelView; + public declare mathView?: MathView; + public override locale: Locale; constructor( locale: Locale, @@ -38,6 +38,7 @@ export default class MainFormView extends View { katexRenderOptions: KatexOptions ) { super( locale ); + this.locale = locale; const t = locale.t; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e470a489..65308fda2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -274,9 +274,6 @@ importers: marked: specifier: 17.0.0 version: 17.0.0 - mathlive: - specifier: 0.108.2 - version: 0.108.2 mermaid: specifier: 11.12.1 version: 11.12.1 @@ -15584,8 +15581,6 @@ snapshots: '@ckeditor/ckeditor5-core': 47.2.0 '@ckeditor/ckeditor5-upload': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-ai@47.2.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: @@ -16016,8 +16011,6 @@ snapshots: '@ckeditor/ckeditor5-table': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-emoji@47.2.0': dependencies: @@ -16502,6 +16495,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-restricted-editing@47.2.0': dependencies: From 4f044c4a57f2b9b7b5d6d4b5724120774e7b5b42 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sun, 23 Nov 2025 22:43:07 +0100 Subject: [PATCH 15/67] Use icons form CKEditor5 icons, instead of testing icons. --- packages/ckeditor5-math/package.json | 1 + packages/ckeditor5-math/src/ui/mainformview.ts | 4 ++-- pnpm-lock.yaml | 9 +++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-math/package.json b/packages/ckeditor5-math/package.json index 0f3e8a37a..34f845f91 100644 --- a/packages/ckeditor5-math/package.json +++ b/packages/ckeditor5-math/package.json @@ -71,6 +71,7 @@ ] }, "dependencies": { + "@ckeditor/ckeditor5-icons": "47.2.0", "mathlive": "0.108.2" } } diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index ac25bc5f0..ab904c86e 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -1,6 +1,6 @@ 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 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 MathLiveInputView from './mathliveinputview.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65308fda2..54486943a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1055,6 +1055,9 @@ importers: packages/ckeditor5-math: dependencies: + '@ckeditor/ckeditor5-icons': + specifier: 47.2.0 + version: 47.2.0 mathlive: specifier: 0.108.2 version: 0.108.2 @@ -15581,6 +15584,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.2.0 '@ckeditor/ckeditor5-upload': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-ai@47.2.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: @@ -16011,6 +16016,8 @@ snapshots: '@ckeditor/ckeditor5-table': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-emoji@47.2.0': dependencies: @@ -16495,8 +16502,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0 ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-restricted-editing@47.2.0': dependencies: From d5e601eae9325f9914d1ac71fd5409aa4c44915a Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Mon, 24 Nov 2025 17:56:18 +0100 Subject: [PATCH 16/67] Simpliyfied resize logic for math input form and improved css --- .../ckeditor5-math/src/ui/mainformview.ts | 78 ----- .../src/ui/mathliveinputview.ts | 5 +- .../src/ui/rawlatexinputview.ts | 6 +- packages/ckeditor5-math/theme/mathform.css | 325 ++++++++---------- 4 files changed, 142 insertions(+), 272 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index ab904c86e..cb665347c 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -161,14 +161,10 @@ export default class MainFormView extends View { if ( this.element ) { this.keystrokes.listenTo( this.element ); } - - this._initResizeSync(); } public override destroy(): void { super.destroy(); - this._resizeObserver?.disconnect(); - document.removeEventListener( 'mouseup', this._onMouseUp ); } public focus(): void { @@ -201,80 +197,6 @@ export default class MainFormView extends View { } } ); - 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( target: 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() { - this.listenTo( this.mathLiveInputView, 'mousedown', () => { - if ( this.mathLiveInputView.element ) { - this._onMouseDown( this.mathLiveInputView.element ); - } - } ); - - this.listenTo( this.rawLatexInputView, 'mousedown', () => { - if ( this.rawLatexInputView.element ) { - this._onMouseDown( this.rawLatexInputView.element ); - } - } ); - - 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. * diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index 9dbce2903..d337a9bdb 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -49,10 +49,7 @@ export default class MathLiveInputView extends View { public override render(): void { super.render(); - // Propagate mousedown event to the view - this.element!.addEventListener( 'mousedown', ( evt ) => { - this.fire( 'mousedown', evt ); - } ); + // (Removed global area click-to-focus logic; focusing now relies on direct field interaction.) // Create the MathLive math-field custom element const mathfield = document.createElement( 'math-field' ) as any; diff --git a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts index 4c994778d..5831fad08 100644 --- a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts +++ b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts @@ -57,10 +57,6 @@ export default class RawLatexInputView extends LabeledFieldView { public override render(): void { super.render(); // All styling is handled via CSS in mathform.css - - // Propagate mousedown event to the view - this.element!.addEventListener( 'mousedown', ( evt ) => { - this.fire( 'mousedown', evt ); - } ); + // (Removed obsolete mousedown propagation; no longer needed after resize & gray-area click removal.) } } diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 300e759d3..3f2546c24 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -1,227 +1,182 @@ /** * Math equation editor dialog styles - * - * This stylesheet provides the layout and styling for the math equation editor, - * which includes a MathLive visual editor, a raw LaTeX textarea, and control buttons. - * The dialog is resizable and uses a flexible layout to accommodate different content sizes. + * Complete version with scrolling fixes for preview and input */ /* ============================================================================ - Form Layout + 1. Main Layout Containers (The Skeleton) ========================================================================= */ .ck.ck-math-form { - display: flex; - flex-direction: column; - padding: var(--ck-spacing-standard); - width: 100%; - height: 100%; - box-sizing: border-box; - - @media screen and (max-width: 600px) { - flex-wrap: wrap; - } + display: flex; + flex-direction: column; + padding: var(--ck-spacing-standard); + box-sizing: border-box; + max-width: 80vw; + height: 100%; /* Never wider than screen */ + overflow-x: hidden; /* Prevent the main window from scrolling horizontally */ } - -/* ============================================================================ - Button Row - ========================================================================= */ - -/* Button row */ -.ck-math-button-row { - display: flex; - gap: var(--ck-spacing-standard); - flex-shrink: 0; - margin-top: var(--ck-spacing-standard); - width: fit-content; +/* Mobile responsiveness */ +@media screen and (max-width: 600px) { + .ck.ck-math-form { + flex-wrap: wrap; + } } -/* Scrollable content area */ -.ck-math-scroll { - display: flex; - flex-direction: column; - flex: 1 1 auto; - min-height: 0; - min-width: 0; - overflow: hidden; -} - -/* ============================================================================ - Math Panel Layout - ========================================================================= */ - -/* Math panel layout */ .ck-math-view { - display: flex; - flex-direction: column; - gap: var(--ck-spacing-standard); - flex: 1 1 auto; - min-height: fit-content; + display: flex; + flex-direction: column; + flex: 1 1 auto; + gap: var(--ck-spacing-standard); + min-height: fit-content; + min-width: 0; + width: 100%; +} + +.ck-math-button-row { + display: flex; + flex-shrink: 0; + gap: var(--ck-spacing-standard); + margin-top: var(--ck-spacing-standard); + width: fit-content; + max-width: 100%; + flex-wrap: wrap; } /* ============================================================================ - MathLive Integration + 2. Shared Styling (Applies to Input AND Preview) ========================================================================= */ -/* MathLive input container */ +/* This targets both the top input AND the bottom preview */ +.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); + border: none !important; + border-radius: var(--ck-border-radius, 6px); + outline: 3px solid transparent; + outline-offset: 6px; +} + +/* SPECIFIC FIX FOR PREVIEW SCROLLING */ +.ck.ck-math-form math-field { + display: block !important; + width: 100%; + + /* 3. Stop it from growing infinite */ + max-width: 100%; + + /* 4. Enable scrollbars for the red matrix area */ + overflow-x: auto !important; + + /* Theme overrides */ + --selection-background-color: rgba(33, 150, 243, 0.2); + --selection-color: inherit; + --contains-highlight-background-color: rgba(0, 0, 0, 0.05); +} + +/* ============================================================================ + 3. MathLive Input Specifics (The Top Box) + ========================================================================= */ + +/* Wrapper for the editable input at the top */ .ck.ck-mathlive-input { - display: inline-block; - flex: 0 0 auto; - width: auto; - min-width: 100%; - min-height: 140px; - max-height: 70vh; - resize: both; - overflow: auto; - padding-bottom: var(--ck-spacing-small); - box-sizing: border-box; + display: inline-block; + flex: 0 0 auto; + width: 100%; + max-width: 100%; /* Safety */ + min-height: fit-content; + max-height: 80vh; + overflow: auto; + padding-bottom: var(--ck-spacing-small); + resize: none; } -/* MathLive field styling */ -.ck.ck-mathlive-input math-field { - display: block !important; - width: 100%; - height: 100%; - min-height: 140px; - box-sizing: border-box; - resize: none !important; - overflow: hidden !important; -} - -/* Style MathLive shadow DOM parts so the whole area is interactive */ +/* Shadow DOM Layout adjustments (Keep your existing logic) */ .ck.ck-math-form math-field::part(container), .ck.ck-math-form math-field::part(content), .ck.ck-math-form math-field::part(field) { - display: flex; - flex-direction: column; - flex: 1 1 auto; - min-height: 100%; - height: 100%; - align-items: flex-start; - justify-content: flex-start; + display: flex; + flex-direction: column; + flex: 1 1 auto; + height: 100%; + align-items: flex-start; + justify-content: flex-start; } - -/* Position MathLive virtual keyboard toggle button */ -.ck.ck-math-form math-field::part(virtual-keyboard-toggle) { - position: absolute; - right: 8px; - top: 8px; -} - -/* Position MathLive menu toggle button and ensure it's always visible */ +/* UI Buttons positions */ +.ck.ck-math-form math-field::part(virtual-keyboard-toggle), .ck.ck-math-form math-field::part(menu-toggle) { - position: absolute; - right: 8px; - top: 48px; - /* Force visibility even when field is empty */ - display: flex !important; - visibility: visible !important; + position: absolute; + top: 8px; +} +.ck.ck-math-form math-field::part(virtual-keyboard-toggle) { right: 40px; } +.ck.ck-math-form math-field::part(menu-toggle) { + right: 8px; + display: flex !important; + visibility: visible !important; } - /* ============================================================================ - Raw LaTeX Integration - ========================================================================= */ - -/* Mirror MathLive container behavior for the labeled textarea wrapper */ - - -.ck-math-view .ck-labeled-field-view { - display: flex; - flex-direction: column; - flex: 0 0 auto; - width: 100%; - min-width: 100%; - min-height: 140px; - max-height: 70vh; - resize: both; - overflow: auto; - padding-bottom: 0; - box-sizing: border-box; - background: transparent; - border: none; - border-radius: 0; - outline: none; -} - -/* Let the internal wrapper stretch so the textarea can fill the space */ -.ck-math-view .ck-labeled-field-view .ck-labeled-field-view__input-wrapper { - display: flex; - flex-direction: column; - flex: 1 1 auto; - width: 100%; - height: auto; - min-height: 100px; - margin: 0; - padding: 0; - background: transparent; - border: none; - box-shadow: none; -} - -/* Ensure the raw textarea fills its wrapper like MathLive's math-field */ -.ck-math-view .ck-labeled-field-view textarea { - display: block; - width: 100% !important; - flex: 1 1 auto; - height: 100%; - min-height: 140px; - box-sizing: border-box; - resize: none !important; - overflow: hidden !important; -} - - -/* ============================================================================ - Shared Input Styling (MathLive & Raw LaTeX) + 4. Raw LaTeX Integration (The Middle Box) ========================================================================= */ -/* Base styling for both MathLive field and raw LaTeX textarea */ -.ck.ck-math-form math-field, -.ck.ck-math-form textarea { - padding: var(--ck-spacing-small); - border: none !important; - border-radius: var(--ck-border-radius, 6px); - font-size: var(--ck-font-size-base); - box-sizing: border-box; - background: var(--ck-color-input-background) !important; - color: var(--ck-color-input-text, inherit); - outline: 3px solid transparent; - outline-offset: 6px; +.ck-math-view .ck-labeled-field-view { + display: flex; + flex-direction: column; + flex: 0 0 auto; + min-width: 100%; + + /* Allow the middle box to shrink if needed */ + width: 100%; + max-width: 100%; + + min-height: 140px; + max-height: 70vh; + resize: both; + overflow: auto; + background: transparent; } -/* Override MathLive selection colors to prevent dark blocks in light mode */ -.ck.ck-math-form math-field { - --selection-background-color: rgba(33, 150, 243, 0.2); - --selection-color: inherit; - --contains-highlight-background-color: rgba(0, 0, 0, 0.05); +/* Hide label */ +.ck-math-view .ck-labeled-field-view .ck-label { + display: none !important; } -/* Make the raw LaTeX textarea flat (no rounded corners or hover animation) */ +/* Internal 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; + padding: 0; + border: none; + background: transparent; + box-shadow: none; +} + +/* The Textarea */ .ck-math-view .ck-labeled-field-view textarea { - border-radius: 0 !important; - outline: none !important; - box-shadow: none !important; - transition: none !important; + display: block; + flex: 1 1 auto; + width: 100% !important; + height: 100%; + min-height: 140px; + resize: none !important; + border-radius: 0 !important; + box-shadow: none !important; + transition: none !important; } .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; - color: var(--ck-color-input-text, inherit) !important; - outline: none !important; - box-shadow: none !important; - transition: none !important; + background: var(--ck-color-input-background) !important; + outline: none !important; + box-shadow: none !important; } - -/* Hide the internal label (we use a separate label element for better layout control) */ -.ck-math-view .ck-labeled-field-view .ck-label { - display: none !important; -} - - - - - From 4110fec94f8458a1613d95c6eaeea679a2d05738 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Mon, 24 Nov 2025 18:28:59 +0100 Subject: [PATCH 17/67] Removed unnecessary declare keyboard --- packages/ckeditor5-math/src/ui/mainformview.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index cb665347c..f44b020ee 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -9,15 +9,15 @@ import '../../theme/mathform.css'; import type { KatexOptions } from '../typings-external.js'; export default class MainFormView extends View { - public declare saveButtonView: ButtonView; - public declare mathLiveInputView: MathLiveInputView; - public declare rawLatexInputView: RawLatexInputView; - public declare rawLatexLabel: LabelView; - public declare displayButtonView: SwitchButtonView; - public declare cancelButtonView: ButtonView; - public declare previewEnabled: boolean; - public declare previewLabel?: LabelView; - public declare mathView?: MathView; + 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; constructor( From e7355dc0e44cbb0d0b71e36e99bcb69d21696109 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Mon, 24 Nov 2025 18:43:52 +0100 Subject: [PATCH 18/67] remove gitignore unneccesary changes --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9321d866f..9ea55440e 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,4 @@ upload .svelte-kit # docs -**/__screenshots__/ +site/ \ No newline at end of file From 9c4301467fd86dc199e3ea01ad9002968b1a06cc Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Mon, 24 Nov 2025 19:46:04 +0100 Subject: [PATCH 19/67] Remove unused icons from ckeditor5-math package --- packages/ckeditor5-math/theme/icons/cancel.svg | 4 ---- packages/ckeditor5-math/theme/icons/check.svg | 3 --- 2 files changed, 7 deletions(-) delete mode 100644 packages/ckeditor5-math/theme/icons/cancel.svg delete mode 100644 packages/ckeditor5-math/theme/icons/check.svg diff --git a/packages/ckeditor5-math/theme/icons/cancel.svg b/packages/ckeditor5-math/theme/icons/cancel.svg deleted file mode 100644 index 6f755ce79..000000000 --- a/packages/ckeditor5-math/theme/icons/cancel.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ckeditor5-math/theme/icons/check.svg b/packages/ckeditor5-math/theme/icons/check.svg deleted file mode 100644 index d62f08d2e..000000000 --- a/packages/ckeditor5-math/theme/icons/check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - From d2052ad236ef49328ebada652c9ef90a533d8802 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Mon, 24 Nov 2025 21:51:59 +0100 Subject: [PATCH 20/67] Disable mathlive sound effects --- packages/ckeditor5-math/src/ui/mathliveinputview.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index d337a9bdb..6b4930805 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -1,5 +1,5 @@ import { View, type Locale } from 'ckeditor5'; -import 'mathlive'; +import { MathfieldElement } from 'mathlive'; /** * A view that wraps the MathLive `` web component for interactive LaTeX equation editing. @@ -48,6 +48,10 @@ export default class MathLiveInputView extends View { */ public override render(): void { super.render(); + // Disable sounds before creating mathfield + if (typeof MathfieldElement !== 'undefined') { + MathfieldElement.soundsDirectory = null; + } // (Removed global area click-to-focus logic; focusing now relies on direct field interaction.) From 51db729546adee649dfde69151e48498103c6b0c Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Tue, 25 Nov 2025 23:27:06 +0100 Subject: [PATCH 21/67] Improve and simplify Mathfield integration --- packages/ckeditor5-math/src/mathui.ts | 24 +- .../ckeditor5-math/src/ui/mainformview.ts | 359 ++++++------------ .../src/ui/mathliveinputview.ts | 81 ++-- packages/ckeditor5-math/src/ui/mathview.ts | 70 ++-- .../src/ui/rawlatexinputview.ts | 18 +- 5 files changed, 207 insertions(+), 345 deletions(-) diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index 504adf77a..a27ee87fd 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -71,22 +71,20 @@ 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.mathLiveInputView.bind( 'value' ).to( mathCommand, 'value' ); @@ -164,9 +162,9 @@ export default class MathUI extends Plugin { // Show preview element const previewEl = document.getElementById( this._previewUid ); - if ( previewEl && this.formView.previewEnabled ) { + if ( previewEl && this.formView.mathView ) { // Force refresh preview - this.formView.mathView?.updateMath(); + this.formView.mathView.updateMath(); } this.formView.equation = mathCommand.value ?? ''; diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index f44b020ee..15e3ed55d 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -1,174 +1,136 @@ -import { ButtonView, FocusCycler, LabelView, submitHandler, SwitchButtonView, View, ViewCollection, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5'; +import { + ButtonView, + FocusCycler, + LabelView, + submitHandler, + SwitchButtonView, + View, + ViewCollection, + type FocusableView, + Locale, + FocusTracker, + KeystrokeHandler +} 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 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 cancelButtonView: ButtonView; + public displayButtonView: SwitchButtonView; + 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; + + 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: string[] = [] ) { super( locale ); - this.locale = locale; - const t = locale.t; - // Submit button - this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null ); + // --- 1. View Initialization --- + + this.mathLiveInputView = new MathLiveInputView( locale ); + this.rawLatexInputView = new RawLatexInputView( locale ); + this.rawLatexInputView.label = t( 'LaTeX' ); + + this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save' ); this.saveButtonView.type = 'submit'; - // MathLive visual equation editor - this.mathLiveInputView = this._createMathLiveInput(); + this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel' ); + this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' ); - // Raw LaTeX input - this.rawLatexInputView = this._createRawLatexInput(); + this.displayButtonView = this._createDisplayButton( t ); - // Raw LaTeX label - this.rawLatexLabel = new LabelView( locale ); - this.rawLatexLabel.text = t( 'LaTeX' ); + // --- 2. Construct Children & Preview --- - // Display button - this.displayButtonView = this._createDisplayButton(); + const children: View[] = [ + this.mathLiveInputView, + this.rawLatexInputView, + 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; + // Clean instantiation using the options object + this.mathView = new MathView( locale, mathViewOptions ); - 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 ); + // Bind display mode: When button flips, preview updates automatically 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 - ]; + children.push( previewLabel, this.mathView ); } - // Add UI elements to template + // --- 3. Sync Logic --- + this._setupInputSync( previewEnabled ); + + // --- 4. Template Setup --- this.setTemplate( { tag: 'form', attributes: { - class: [ - 'ck', - 'ck-math-form', - ...popupClassName - ], + 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 - } - ] + 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 - ] + attributes: { class: [ 'ck-math-button-row' ] }, + children: [ this.saveButtonView, this.cancelButtonView ] } ] } ); + + // --- 5. Accessibility --- + this._focusCycler = new FocusCycler( { + focusables: this._focusables, + focusTracker: this.focusTracker, + keystrokeHandler: this.keystrokes, + actions: { focusPrevious: 'shift + tab', focusNext: 'tab' } + } ); } public override render(): void { super.render(); - // Prevent default form submit event & trigger custom 'submit' - submitHandler( { - view: this - } ); + submitHandler( { view: this } ); - // Register form elements to focusable elements - const childViews = [ + // Register focusables + [ this.mathLiveInputView, this.rawLatexInputView, this.displayButtonView, this.saveButtonView, this.cancelButtonView - ]; - - childViews.forEach( v => { + ].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 ); - } - } - - public override destroy(): void { - super.destroy(); - } - - public focus(): void { - this._focusCycler.focusFirst(); + if ( this.element ) this.keystrokes.listenTo( this.element ); } public get equation(): string { @@ -176,151 +138,82 @@ export default class MainFormView extends View { } 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; - } + const norm = equation.trim(); + // Direct updates to the "source of truth" + this.mathLiveInputView.value = norm.length ? norm : null; + this.rawLatexInputView.value = norm; + if ( this.mathView ) this.mathView.value = norm; } - 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' - } - } ); + public focus(): void { + this._focusCycler.focusFirst(); + } /** - * Creates the MathLive visual equation editor. - * - * Handles bidirectional synchronization with the raw LaTeX textarea and preview. + * Sets up split handlers for synchronization. */ - private _createMathLiveInput() { - const mathLiveInput = new MathLiveInputView( this.locale ); + private _setupInputSync( previewEnabled: boolean ): void { + // Handler 1: MathLive -> Raw LaTeX + this.mathLiveInputView.on( 'change:value', () => { + let eq = ( this.mathLiveInputView.value ?? '' ).trim(); - const onInput = () => { - let equationInput = ( mathLiveInput.value ?? '' ).trim(); - - // If input has delimiters, strip them and update the display mode. - if ( hasDelimiters( equationInput ) ) { - const params = extractDelimiters( equationInput ); - equationInput = params.equation; + // Delimiter Normalization + if ( hasDelimiters( eq ) ) { + const params = extractDelimiters( eq ); + eq = params.equation; this.displayButtonView.isOn = params.display; + + // UX Fix: If we stripped delimiters, update the source + // so the visual editor doesn't show them. + if ( this.mathLiveInputView.value !== eq ) { + this.mathLiveInputView.value = eq; + } } - const normalizedEquation = equationInput.length ? equationInput : null; - - // Update self if needed. - if ( mathLiveInput.value !== normalizedEquation ) { - mathLiveInput.value = normalizedEquation; + // Sync to Raw LaTeX + if ( this.rawLatexInputView.value !== eq ) { + this.rawLatexInputView.value = eq; } - // Sync to raw LaTeX textarea if its value is different. - if ( this.rawLatexInputView.value !== equationInput ) { - this.rawLatexInputView.value = equationInput; + // Sync to Preview + if ( previewEnabled && this.mathView && this.mathView.value !== eq ) { + this.mathView.value = eq; + } + } ); + + // Handler 2: Raw LaTeX -> MathLive + this.rawLatexInputView.on( 'change:value', () => { + const eq = ( this.rawLatexInputView.value ?? '' ).trim(); + const normalized = eq.length ? eq : null; + + // Sync to MathLive + if ( this.mathLiveInputView.value !== normalized ) { + this.mathLiveInputView.value = normalized; } - // Update preview if enabled and its value is different. - if ( this.previewEnabled && this.mathView && this.mathView.value !== equationInput ) { - this.mathView.value = equationInput; + // Sync to Preview + if ( previewEnabled && this.mathView && this.mathView.value !== eq ) { + this.mathView.value = eq; } - }; - - 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 ): ButtonView { + const btn = new ButtonView( this.locale ); + btn.set( { label, icon, tooltip: true } ); + btn.extendTemplate( { attributes: { class: className } } ); + return btn; } - private _createButton( - label: string, - icon: string, - className: string, - eventName: string | null - ) { - const button = new ButtonView( this.locale ); + private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView { + const btn = new SwitchButtonView( this.locale ); + btn.set( { label: t( 'Display mode' ), withText: true } ); + btn.extendTemplate( { attributes: { class: 'ck-button-display-toggle' } } ); - button.set( { - label, - icon, - tooltip: true + btn.on( 'execute', () => { + btn.isOn = !btn.isOn; + // mathView updates automatically via bind() } ); - - 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; + return btn; } } diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index 6b4930805..b5f36e94e 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -1,33 +1,27 @@ import { View, type Locale } from 'ckeditor5'; -import { MathfieldElement } from 'mathlive'; +import 'mathlive'; // Import side-effects only (registers the tag) /** - * A view that wraps the MathLive `` web component for interactive LaTeX equation editing. - * - * MathLive provides a rich math input experience with live rendering, virtual keyboard support, - * and various accessibility features. - * - * @see https://cortexjs.io/mathlive/ + * A wrapper for the MathLive component. + * Uses 'any' typing to avoid TypeScript module resolution errors. */ export default class MathLiveInputView extends View { /** - * The current LaTeX value of the math field. - * + * The current LaTeX value. * @observable */ public declare value: string | null; /** - * Whether the input is in read-only mode. - * + * Read-only state. * @observable */ public declare isReadOnly: boolean; /** - * Reference to the `` DOM element. + * Reference to the DOM element (typed as any to prevent TS errors). */ - public mathfield: HTMLElement | null = null; + public mathfield: any = null; constructor( locale: Locale ) { super( locale ); @@ -43,68 +37,53 @@ export default class MathLiveInputView extends View { } ); } - /** - * @inheritDoc - */ public override render(): void { super.render(); - // Disable sounds before creating mathfield - if (typeof MathfieldElement !== 'undefined') { - MathfieldElement.soundsDirectory = null; - } - // (Removed global area click-to-focus logic; focusing now relies on direct field interaction.) - - // Create the MathLive math-field custom element + // 1. Create element using DOM API instead of Class constructor + // This avoids "Module has no exported member" errors. const mathfield = document.createElement( 'math-field' ) as any; - this.mathfield = mathfield; - // Configure the virtual keyboard to be manually controlled (shown by user interaction) - mathfield.setAttribute( 'virtual-keyboard-mode', 'manual' ); + // 2. Configure Options + mathfield.mathVirtualKeyboardPolicy = 'manual'; - // Set initial value - const initialValue = this.value ?? ''; - if ( initialValue ) { - ( mathfield as any ).value = initialValue; + // Disable sounds + const MathfieldElement = customElements.get( 'math-field' ); + if ( MathfieldElement ) { + ( MathfieldElement as any ).soundsDirectory = null; + ( MathfieldElement as any ).plonkSound = null; } - // Bind readonly state - if ( this.isReadOnly ) { - ( mathfield as any ).readOnly = true; - } + // 3. Set Initial State + mathfield.value = this.value ?? ''; + mathfield.readOnly = this.isReadOnly; - // Sync math-field changes to observable value + // 4. Bind Events (DOM -> Observable) mathfield.addEventListener( 'input', () => { - const nextValue: string = ( mathfield as any ).value; - this.value = nextValue.length ? nextValue : null; + const val = mathfield.value; + this.value = val.length ? val : null; } ); - // Sync observable value changes back to math-field - this.on( 'change:value', () => { - const nextValue = this.value ?? ''; - if ( ( mathfield as any ).value !== nextValue ) { - ( mathfield as any ).value = nextValue; + // 5. Bind Events (Observable -> DOM) + this.on( 'change:value', ( _evt, _name, nextValue ) => { + if ( mathfield.value !== nextValue ) { + mathfield.value = nextValue ?? ''; } } ); - // Sync readonly state to math-field - this.on( 'change:isReadOnly', () => { - ( mathfield as any ).readOnly = this.isReadOnly; + this.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => { + mathfield.readOnly = nextValue; } ); + // 6. Mount this.element?.appendChild( mathfield ); + this.mathfield = mathfield; } - /** - * Focuses the math-field element. - */ public focus(): void { this.mathfield?.focus(); } - /** - * @inheritDoc - */ public override destroy(): void { if ( this.mathfield ) { this.mathfield.remove(); diff --git a/packages/ckeditor5-math/src/ui/mathview.ts b/packages/ckeditor5-math/src/ui/mathview.ts index fab16262e..254c306ab 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(); @@ -59,13 +59,13 @@ export default class MathView extends View { void renderEquation( this.value, this.element, - this.engine, - this.lazyLoad, + this.options.engine, + this.options.lazyLoad, this.display, - true, - this.previewUid, - this.previewClassName, - this.katexRenderOptions + true, // isPreview + this.options.previewUid, + this.options.previewClassName, + this.options.katexRenderOptions ); } } diff --git a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts index 5831fad08..81593ec76 100644 --- a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts +++ b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts @@ -2,21 +2,16 @@ import { LabeledFieldView, createLabeledTextarea, type Locale, type TextareaView /** * A labeled textarea view for direct LaTeX code editing. - * - * This provides a plain text input for users who prefer to write LaTeX syntax directly - * or need to paste/edit raw LaTeX code. */ export default class RawLatexInputView extends LabeledFieldView { /** * The current LaTeX value. - * * @observable */ public declare value: string; /** * Whether the input is in read-only mode. - * * @observable */ public declare isReadOnly: boolean; @@ -29,21 +24,23 @@ export default class RawLatexInputView extends LabeledFieldView { const fieldView = this.fieldView; - // Sync textarea input to observable value + // 1. Sync: DOM (Textarea) -> Observable + // We listen to the native 'input' event on the child view fieldView.on( 'input', () => { if ( fieldView.element ) { this.value = fieldView.element.value; } } ); - // Sync observable value changes back to textarea + // 2. Sync: Observable -> DOM (Textarea) this.on( 'change:value', () => { + // Check for difference to avoid cursor jumping or unnecessary updates if ( fieldView.element && fieldView.element.value !== this.value ) { fieldView.element.value = this.value; } } ); - // Sync readonly state (manual binding to avoid CKEditor observable rebind error) + // 3. Sync: ReadOnly State this.on( 'change:isReadOnly', () => { if ( fieldView.element ) { fieldView.element.readOnly = this.isReadOnly; @@ -51,12 +48,7 @@ export default class RawLatexInputView extends LabeledFieldView { } ); } - /** - * @inheritDoc - */ public override render(): void { super.render(); - // All styling is handled via CSS in mathform.css - // (Removed obsolete mousedown propagation; no longer needed after resize & gray-area click removal.) } } From c8d34e65ea230d45a1a627345f899218f91a8d34 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Wed, 26 Nov 2025 21:49:09 +0100 Subject: [PATCH 22/67] Improve max window size --- packages/ckeditor5-math/theme/mathform.css | 86 +++++++++++++++------- 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 3f2546c24..270e86568 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -1,10 +1,10 @@ /** * Math equation editor dialog styles - * Complete version with scrolling fixes for preview and input + * Supports MathLive input, raw LaTeX textarea, and equation preview */ /* ============================================================================ - 1. Main Layout Containers (The Skeleton) + Main Dialog Container ========================================================================= */ .ck.ck-math-form { @@ -12,9 +12,10 @@ flex-direction: column; padding: var(--ck-spacing-standard); box-sizing: border-box; - max-width: 80vw; - height: 100%; /* Never wider than screen */ - overflow-x: hidden; /* Prevent the main window from scrolling horizontally */ + max-width: 80vw; + max-height: 80vh; + height: 100%; + overflow-x: hidden; } /* Mobile responsiveness */ @@ -24,6 +25,10 @@ } } +/* ============================================================================ + Content Layout + ========================================================================= */ + .ck-math-view { display: flex; flex-direction: column; @@ -34,6 +39,36 @@ width: 100%; } +/* 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; @@ -45,10 +80,10 @@ } /* ============================================================================ - 2. Shared Styling (Applies to Input AND Preview) + Shared Styles for Input Fields ========================================================================= */ -/* This targets both the top input AND the bottom preview */ +/* Base styling for both MathLive fields and textareas */ .ck.ck-math-form math-field, .ck.ck-math-form textarea { box-sizing: border-box; @@ -62,33 +97,28 @@ outline-offset: 6px; } -/* SPECIFIC FIX FOR PREVIEW SCROLLING */ +/* MathLive-specific configuration */ .ck.ck-math-form math-field { display: block !important; width: 100%; - - /* 3. Stop it from growing infinite */ max-width: 100%; - - /* 4. Enable scrollbars for the red matrix area */ overflow-x: auto !important; - /* Theme overrides */ + /* MathLive theme customization */ --selection-background-color: rgba(33, 150, 243, 0.2); --selection-color: inherit; --contains-highlight-background-color: rgba(0, 0, 0, 0.05); } /* ============================================================================ - 3. MathLive Input Specifics (The Top Box) + MathLive Visual Editor (Top Input) ========================================================================= */ -/* Wrapper for the editable input at the top */ .ck.ck-mathlive-input { display: inline-block; flex: 0 0 auto; width: 100%; - max-width: 100%; /* Safety */ + max-width: 100%; min-height: fit-content; max-height: 80vh; overflow: auto; @@ -96,7 +126,7 @@ resize: none; } -/* Shadow DOM Layout adjustments (Keep your existing logic) */ +/* Configure MathLive shadow DOM layout */ .ck.ck-math-form math-field::part(container), .ck.ck-math-form math-field::part(content), .ck.ck-math-form math-field::part(field) { @@ -108,13 +138,17 @@ justify-content: flex-start; } -/* UI Buttons positions */ +/* Position MathLive UI controls */ .ck.ck-math-form math-field::part(virtual-keyboard-toggle), .ck.ck-math-form math-field::part(menu-toggle) { position: absolute; top: 8px; } -.ck.ck-math-form math-field::part(virtual-keyboard-toggle) { right: 40px; } + +.ck.ck-math-form math-field::part(virtual-keyboard-toggle) { + right: 40px; +} + .ck.ck-math-form math-field::part(menu-toggle) { right: 8px; display: flex !important; @@ -122,7 +156,7 @@ } /* ============================================================================ - 4. Raw LaTeX Integration (The Middle Box) + Raw LaTeX Textarea (Middle Input) ========================================================================= */ .ck-math-view .ck-labeled-field-view { @@ -130,24 +164,21 @@ flex-direction: column; flex: 0 0 auto; min-width: 100%; - - /* Allow the middle box to shrink if needed */ width: 100%; max-width: 100%; - min-height: 140px; - max-height: 70vh; + max-height: 65vh; resize: both; overflow: auto; background: transparent; } -/* Hide label */ +/* Hide the default label (we use ::before for custom heading) */ .ck-math-view .ck-labeled-field-view .ck-label { display: none !important; } -/* Internal wrapper */ +/* Textarea wrapper */ .ck-math-view .ck-labeled-field-view .ck-labeled-field-view__input-wrapper { display: flex; flex-direction: column; @@ -161,7 +192,7 @@ box-shadow: none; } -/* The Textarea */ +/* Raw LaTeX textarea styling */ .ck-math-view .ck-labeled-field-view textarea { display: block; flex: 1 1 auto; @@ -174,6 +205,7 @@ 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; From a6de1041c7abb10c358259ece2098e0abc003c18 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Wed, 26 Nov 2025 21:59:33 +0100 Subject: [PATCH 23/67] Fix bug in math rendering where old content was not cleared --- packages/ckeditor5-math/src/ui/mathview.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ckeditor5-math/src/ui/mathview.ts b/packages/ckeditor5-math/src/ui/mathview.ts index 254c306ab..42a4c0df1 100644 --- a/packages/ckeditor5-math/src/ui/mathview.ts +++ b/packages/ckeditor5-math/src/ui/mathview.ts @@ -56,6 +56,10 @@ export default class MathView extends View { public updateMath(): void { if ( this.element ) { + + // This prevents the new render from appending to the old one. + this.element.textContent = ''; + void renderEquation( this.value, this.element, From 64ab1c41162cc3043889a706e38fc789ccc0431a Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Wed, 26 Nov 2025 22:29:29 +0100 Subject: [PATCH 24/67] Imrovement for Latex --- packages/ckeditor5-math/theme/mathform.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 270e86568..c2a1476bb 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -166,7 +166,7 @@ min-width: 100%; width: 100%; max-width: 100%; - min-height: 140px; + min-height: 60px; max-height: 65vh; resize: both; overflow: auto; From c46cf418426bf7cf6d04c8b73a2bd6449061015b Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Wed, 26 Nov 2025 22:48:57 +0100 Subject: [PATCH 25/67] Small improvements --- .../src/ui/mathliveinputview.ts | 149 ++++++++++-------- .../src/ui/rawlatexinputview.ts | 78 ++++----- 2 files changed, 118 insertions(+), 109 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index b5f36e94e..a3863077c 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -1,94 +1,103 @@ import { View, type Locale } from 'ckeditor5'; import 'mathlive'; // Import side-effects only (registers the tag) +/** + * Interface describing the custom element. + */ +interface MathFieldElement extends HTMLElement { + value: string; + readOnly: boolean; + mathVirtualKeyboardPolicy: string; + +} + /** * A wrapper for the MathLive component. - * Uses 'any' typing to avoid TypeScript module resolution errors. */ export default class MathLiveInputView extends View { - /** - * The current LaTeX value. - * @observable - */ - public declare value: string | null; + /** + * The current LaTeX value. + * @observable + */ + public declare value: string | null; - /** - * Read-only state. - * @observable - */ - public declare isReadOnly: boolean; + /** + * Read-only state. + * @observable + */ + public declare isReadOnly: boolean; - /** - * Reference to the DOM element (typed as any to prevent TS errors). - */ - public mathfield: any = null; + /** + * Reference to the DOM element. + * Typed as MathFieldElement | null for proper TS support. + */ + public mathfield: MathFieldElement | null = null; - constructor( locale: Locale ) { - super( locale ); + constructor( locale: Locale ) { + super( locale ); - this.set( 'value', null ); - this.set( 'isReadOnly', false ); + this.set( 'value', null ); + this.set( 'isReadOnly', false ); - this.setTemplate( { - tag: 'div', - attributes: { - class: [ 'ck', 'ck-mathlive-input' ] - } - } ); - } + this.setTemplate( { + tag: 'div', + attributes: { + class: [ 'ck', 'ck-mathlive-input' ] + } + } ); + } - public override render(): void { - super.render(); + public override render(): void { + super.render(); - // 1. Create element using DOM API instead of Class constructor - // This avoids "Module has no exported member" errors. - const mathfield = document.createElement( 'math-field' ) as any; + // 1. Create element with the specific type + const mathfield = document.createElement( 'math-field' ) as MathFieldElement; - // 2. Configure Options - mathfield.mathVirtualKeyboardPolicy = 'manual'; + // 2. Configure Options + mathfield.mathVirtualKeyboardPolicy = 'manual'; - // Disable sounds - const MathfieldElement = customElements.get( 'math-field' ); - if ( MathfieldElement ) { - ( MathfieldElement as any ).soundsDirectory = null; - ( MathfieldElement as any ).plonkSound = null; - } + // Disable sounds + const MathfieldConstructor = customElements.get( 'math-field' ); + if ( MathfieldConstructor ) { + ( MathfieldConstructor as any ).soundsDirectory = null; + ( MathfieldConstructor as any ).plonkSound = null; + } - // 3. Set Initial State - mathfield.value = this.value ?? ''; - mathfield.readOnly = this.isReadOnly; + // 3. Set Initial State + mathfield.value = this.value ?? ''; + mathfield.readOnly = this.isReadOnly; - // 4. Bind Events (DOM -> Observable) - mathfield.addEventListener( 'input', () => { - const val = mathfield.value; - this.value = val.length ? val : null; - } ); + // 4. Bind Events (DOM -> Observable) + mathfield.addEventListener( 'input', () => { + const val = mathfield.value; + this.value = val.length ? val : null; + } ); - // 5. Bind Events (Observable -> DOM) - this.on( 'change:value', ( _evt, _name, nextValue ) => { - if ( mathfield.value !== nextValue ) { - mathfield.value = nextValue ?? ''; - } - } ); + // 5. Bind Events (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.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => { + mathfield.readOnly = nextValue; + } ); - // 6. Mount - this.element?.appendChild( mathfield ); - this.mathfield = mathfield; - } + // 6. Mount + this.element?.appendChild( mathfield ); + this.mathfield = mathfield; + } - public focus(): void { - this.mathfield?.focus(); - } + public focus(): void { + this.mathfield?.focus(); + } - public override destroy(): void { - if ( this.mathfield ) { - this.mathfield.remove(); - this.mathfield = null; - } - super.destroy(); - } + public override destroy(): void { + if ( this.mathfield ) { + this.mathfield.remove(); + this.mathfield = null; + } + super.destroy(); + } } diff --git a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts index 81593ec76..cc468b434 100644 --- a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts +++ b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts @@ -4,51 +4,51 @@ import { LabeledFieldView, createLabeledTextarea, type Locale, type TextareaView * A labeled textarea view for direct LaTeX code editing. */ export default class RawLatexInputView extends LabeledFieldView { - /** - * The current LaTeX value. - * @observable - */ - public declare value: string; + /** + * The current LaTeX value. + * @observable + */ + public declare value: string; - /** - * Whether the input is in read-only mode. - * @observable - */ - public declare isReadOnly: boolean; + /** + * Whether the input is in read-only mode. + * @observable + */ + public declare isReadOnly: boolean; - constructor( locale: Locale ) { - super( locale, createLabeledTextarea ); + constructor( locale: Locale ) { + super( locale, createLabeledTextarea ); - this.set( 'value', '' ); - this.set( 'isReadOnly', false ); + this.set( 'value', '' ); + this.set( 'isReadOnly', false ); - const fieldView = this.fieldView; + const fieldView = this.fieldView; - // 1. Sync: DOM (Textarea) -> Observable - // We listen to the native 'input' event on the child view - fieldView.on( 'input', () => { - if ( fieldView.element ) { - this.value = fieldView.element.value; - } - } ); + // 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', () => { - // Check for difference to avoid cursor jumping or unnecessary updates - if ( fieldView.element && fieldView.element.value !== this.value ) { - fieldView.element.value = this.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', () => { - if ( fieldView.element ) { - fieldView.element.readOnly = this.isReadOnly; - } - } ); - } + // 3. Sync: ReadOnly State + this.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => { + fieldView.isReadOnly = nextValue; + } ); + } - public override render(): void { - super.render(); - } + public override render(): void { + super.render(); + } } From f8d84814e079bdca892c1c2cac75c53baa96f20b Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Wed, 26 Nov 2025 23:02:34 +0100 Subject: [PATCH 26/67] Fix differential d problems --- .../src/ui/mathliveinputview.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index a3863077c..f761be787 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -8,7 +8,8 @@ interface MathFieldElement extends HTMLElement { value: string; readOnly: boolean; mathVirtualKeyboardPolicy: string; - + // Interface includes the shortcuts property + inlineShortcuts: Record; } /** @@ -56,11 +57,23 @@ export default class MathLiveInputView extends View { // 2. Configure Options mathfield.mathVirtualKeyboardPolicy = 'manual'; - // Disable sounds + //Disable differential D + mathfield.addEventListener( 'mount', () => { + mathfield.inlineShortcuts = { + ...mathfield.inlineShortcuts, // Safe to read now + dx: 'dx', + dy: 'dy', + dt: 'dt' + }; + } ); + + + // Disable sounds safely const MathfieldConstructor = customElements.get( 'math-field' ); if ( MathfieldConstructor ) { - ( MathfieldConstructor as any ).soundsDirectory = null; - ( MathfieldConstructor as any ).plonkSound = null; + const proto = MathfieldConstructor as any; + if ( proto.soundsDirectory !== null ) proto.soundsDirectory = null; + if ( proto.plonkSound !== null ) proto.plonkSound = null; } // 3. Set Initial State @@ -84,7 +97,7 @@ export default class MathLiveInputView extends View { mathfield.readOnly = nextValue; } ); - // 6. Mount + // 6. Mount to the wrapper view this.element?.appendChild( mathfield ); this.mathfield = mathfield; } From acca22f3a1594bfeb17a28a8b9aec40eb20473a2 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Tue, 2 Dec 2025 22:28:16 +0100 Subject: [PATCH 27/67] Improve Synchronization Between Mathlive and rawlatex input --- .../ckeditor5-math/src/ui/mainformview.ts | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 15e3ed55d..d7ceb2f20 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -48,8 +48,7 @@ export default class MainFormView extends View { this.rawLatexInputView = new RawLatexInputView( locale ); this.rawLatexInputView.label = t( 'LaTeX' ); - this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save' ); - this.saveButtonView.type = 'submit'; + this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', 'submit' ); this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel' ); this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' ); @@ -150,58 +149,67 @@ export default class MainFormView extends View { } /** - * Sets up split handlers for synchronization. + * Checks if a view currently has focus. */ - private _setupInputSync( previewEnabled: boolean ): void { - // Handler 1: MathLive -> Raw LaTeX - this.mathLiveInputView.on( 'change:value', () => { - let eq = ( this.mathLiveInputView.value ?? '' ).trim(); + private _isViewFocused(view: View): boolean { + const el = view.element; + const active = document.activeElement; + return !!(el && active && el.contains(active)); + } - // Delimiter Normalization - if ( hasDelimiters( eq ) ) { - const params = extractDelimiters( eq ); + /** + * Sets up synchronization with Focus Gating. + */ + private _setupInputSync(previewEnabled: boolean): void { + const updatePreview = (eq: string) => { + if (previewEnabled && this.mathView && this.mathView.value !== eq) { + this.mathView.value = eq; + } + }; + + // Handler 1: MathLive -> Raw LaTeX + Preview + this.mathLiveInputView.on('change:value', () => { + let eq = (this.mathLiveInputView.value ?? '').trim(); + + // Strip delimiters if present (e.g. pasted content) + if (hasDelimiters(eq)) { + const params = extractDelimiters(eq); eq = params.equation; this.displayButtonView.isOn = params.display; - // UX Fix: If we stripped delimiters, update the source - // so the visual editor doesn't show them. - if ( this.mathLiveInputView.value !== eq ) { + // Only strip delimiters if not actively editing + if (!this._isViewFocused(this.mathLiveInputView) && this.mathLiveInputView.value !== eq) { this.mathLiveInputView.value = eq; } } - // Sync to Raw LaTeX - if ( this.rawLatexInputView.value !== eq ) { + // Sync to Raw LaTeX only if user isn't typing there + if (!this._isViewFocused(this.rawLatexInputView) && this.rawLatexInputView.value !== eq) { this.rawLatexInputView.value = eq; } - // Sync to Preview - if ( previewEnabled && this.mathView && this.mathView.value !== eq ) { - this.mathView.value = eq; - } - } ); + updatePreview(eq); + }); - // Handler 2: Raw LaTeX -> MathLive - this.rawLatexInputView.on( 'change:value', () => { - const eq = ( this.rawLatexInputView.value ?? '' ).trim(); + // Handler 2: Raw LaTeX -> MathLive + Preview + this.rawLatexInputView.on('change:value', () => { + const eq = (this.rawLatexInputView.value ?? '').trim(); const normalized = eq.length ? eq : null; - // Sync to MathLive - if ( this.mathLiveInputView.value !== normalized ) { + // Sync to MathLive only if user isn't interacting with it + if (!this._isViewFocused(this.mathLiveInputView) && this.mathLiveInputView.value !== normalized) { this.mathLiveInputView.value = normalized; } - // Sync to Preview - if ( previewEnabled && this.mathView && this.mathView.value !== eq ) { - this.mathView.value = eq; - } - } ); + updatePreview(eq); + }); } - private _createButton( label: string, icon: string, className: string ): ButtonView { + private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView { const btn = new ButtonView( this.locale ); btn.set( { label, icon, tooltip: true } ); btn.extendTemplate( { attributes: { class: className } } ); + if (type) btn.type = type; return btn; } From 9386465de7e549a7c57ea8486abc2c2162f8b9ae Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Tue, 2 Dec 2025 22:29:20 +0100 Subject: [PATCH 28/67] Added mathrender error class for better error handling in math rendering --- packages/ckeditor5-math/theme/mathform.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index c2a1476bb..1465b9e1b 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -212,3 +212,13 @@ outline: none !important; box-shadow: none !important; } + +.ck-math-render-error { + color: var(--ck-color-error-text, #db1d1d); + padding: var(--ck-spacing-small); + font-style: italic; + font-size: 0.9em; + border: 1px dashed var(--ck-color-error-text, #db1d1d); + border-radius: 2px; + background: var(--ck-color-base-background, #fff); +} From 162c076a145194b80f3275b4f85fbe790d0a332b Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Tue, 2 Dec 2025 22:30:37 +0100 Subject: [PATCH 29/67] Improve MathLive integration and lazy loading --- .../src/ui/mathliveinputview.ts | 114 +++++++++--------- packages/ckeditor5-math/src/ui/mathview.ts | 48 +++++--- packages/ckeditor5-math/tests/lazyload.ts | 10 ++ 3 files changed, 96 insertions(+), 76 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts index f761be787..a2d540228 100644 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathliveinputview.ts @@ -1,104 +1,98 @@ import { View, type Locale } from 'ckeditor5'; -import 'mathlive'; // Import side-effects only (registers the tag) -/** - * Interface describing the custom element. - */ interface MathFieldElement extends HTMLElement { value: string; readOnly: boolean; mathVirtualKeyboardPolicy: string; - // Interface includes the shortcuts property - inlineShortcuts: Record; + inlineShortcuts?: Record; } -/** - * A wrapper for the MathLive component. - */ export default class MathLiveInputView extends View { - /** - * The current LaTeX value. - * @observable - */ public declare value: string | null; - - /** - * Read-only state. - * @observable - */ public declare isReadOnly: boolean; - - /** - * Reference to the DOM element. - * Typed as MathFieldElement | null for proper TS support. - */ public mathfield: MathFieldElement | null = null; - constructor( locale: Locale ) { - super( locale ); + constructor(locale: Locale) { + super(locale); + this.set('value', null); + this.set('isReadOnly', false); - this.set( 'value', null ); - this.set( 'isReadOnly', false ); - - this.setTemplate( { + this.setTemplate({ tag: 'div', attributes: { - class: [ 'ck', 'ck-mathlive-input' ] + class: ['ck', 'ck-mathlive-input'] } - } ); + }); } public override render(): void { super.render(); + this._loadMathLive(); + } - // 1. Create element with the specific type - const mathfield = document.createElement( 'math-field' ) as MathFieldElement; + private async _loadMathLive(): Promise { + try { + await import('mathlive'); + await customElements.whenDefined('math-field'); - // 2. Configure Options + // Configure global MathLive settings + const MathfieldClass = customElements.get( 'math-field' ) as any; + if ( MathfieldClass ) { + MathfieldClass.soundsDirectory = null; + MathfieldClass.plonkSound = null; + } + + if (!this.element) return; + + this._createMathField(); + } catch (error) { + console.error('MathLive load failed:', error); + if (this.element) { + this.element.textContent = 'Math editor unavailable'; + } + } + } + + private _createMathField(): void { + if (!this.element) return; + + const mathfield = document.createElement('math-field') as MathFieldElement; + + // Instance-level config (no prototype pollution) mathfield.mathVirtualKeyboardPolicy = 'manual'; - //Disable differential D - mathfield.addEventListener( 'mount', () => { + // Configure shortcuts after mount + mathfield.addEventListener('mount', () => { mathfield.inlineShortcuts = { - ...mathfield.inlineShortcuts, // Safe to read now + ...mathfield.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' }; - } ); + }, { once: true }); - - // Disable sounds safely - const MathfieldConstructor = customElements.get( 'math-field' ); - if ( MathfieldConstructor ) { - const proto = MathfieldConstructor as any; - if ( proto.soundsDirectory !== null ) proto.soundsDirectory = null; - if ( proto.plonkSound !== null ) proto.plonkSound = null; - } - - // 3. Set Initial State + // Initial state mathfield.value = this.value ?? ''; mathfield.readOnly = this.isReadOnly; - // 4. Bind Events (DOM -> Observable) - mathfield.addEventListener( 'input', () => { + // DOM -> Observable + mathfield.addEventListener('input', () => { const val = mathfield.value; this.value = val.length ? val : null; - } ); + }); - // 5. Bind Events (Observable -> DOM) - this.on( 'change:value', ( _evt, _name, nextValue ) => { - if ( mathfield.value !== nextValue ) { + // Observable -> DOM + this.on('change:value', (_evt, _name, nextValue) => { + if (mathfield.value !== nextValue) { mathfield.value = nextValue ?? ''; } - } ); + }); - this.on( 'change:isReadOnly', ( _evt, _name, nextValue ) => { + this.on('change:isReadOnly', (_evt, _name, nextValue) => { mathfield.readOnly = nextValue; - } ); + }); - // 6. Mount to the wrapper view - this.element?.appendChild( mathfield ); + this.element.appendChild(mathfield); this.mathfield = mathfield; } @@ -107,7 +101,7 @@ export default class MathLiveInputView extends View { } public override destroy(): void { - if ( this.mathfield ) { + if (this.mathfield) { this.mathfield.remove(); this.mathfield = null; } diff --git a/packages/ckeditor5-math/src/ui/mathview.ts b/packages/ckeditor5-math/src/ui/mathview.ts index 42a4c0df1..87af15d08 100644 --- a/packages/ckeditor5-math/src/ui/mathview.ts +++ b/packages/ckeditor5-math/src/ui/mathview.ts @@ -55,23 +55,39 @@ export default class MathView extends View { } public updateMath(): void { - if ( this.element ) { - - // This prevents the new render from appending to the old one. - this.element.textContent = ''; - - void renderEquation( - this.value, - this.element, - this.options.engine, - this.options.lazyLoad, - this.display, - true, // isPreview - this.options.previewUid, - this.options.previewClassName, - this.options.katexRenderOptions - ); + if (!this.element) { + return; } + + // Handle empty equations + if (!this.value || !this.value.trim()) { + this.element.textContent = ''; + this.element.classList.remove('ck-math-render-error'); + return; + } + + // Clear previous render + this.element.textContent = ''; + this.element.classList.remove('ck-math-render-error'); + + renderEquation( + this.value, + this.element, + this.options.engine, + this.options.lazyLoad, + this.display, + true, // isPreview + this.options.previewUid, + this.options.previewClassName, + this.options.katexRenderOptions + ).catch(error => { + console.error('Math rendering failed:', error); + + if (this.element) { + this.element.textContent = 'Error rendering equation'; + this.element.classList.add('ck-math-render-error'); + } + }); } public override render(): void { diff --git a/packages/ckeditor5-math/tests/lazyload.ts b/packages/ckeditor5-math/tests/lazyload.ts index 126507850..573984368 100644 --- a/packages/ckeditor5-math/tests/lazyload.ts +++ b/packages/ckeditor5-math/tests/lazyload.ts @@ -37,6 +37,7 @@ describe( 'Lazy load', () => { await buildEditor( { math: { engine: 'katex', + enablePreview: true, lazyLoad: async () => { lazyLoadInvoked = true; } @@ -44,6 +45,15 @@ describe( 'Lazy load', () => { } ); mathUIFeature._showUI(); + + // Trigger render with a non-empty value to bypass empty check optimization + if ( mathUIFeature.formView ) { + mathUIFeature.formView.equation = 'x^2'; + } + + // Wait for async rendering and lazy loading + await new Promise( resolve => setTimeout( resolve, 100 ) ); + expect( lazyLoadInvoked ).to.be.true; } ); } ); From 827c8e0e7288b648a511e91becef8f818370e6fc Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Sun, 7 Dec 2025 23:19:48 +0100 Subject: [PATCH 30/67] Refactor: Combine MathLive and LaTeX inputs into one single component --- packages/ckeditor5-math/src/mathui.ts | 26 +-- .../ckeditor5-math/src/ui/mainformview.ts | 92 ++------ .../ckeditor5-math/src/ui/mathinputview.ts | 198 +++++++++++++++++ .../src/ui/mathliveinputview.ts | 110 ---------- .../src/ui/rawlatexinputview.ts | 54 ----- packages/ckeditor5-math/tests/index.ts | 14 ++ packages/ckeditor5-math/tests/lazyload.ts | 21 +- packages/ckeditor5-math/tests/mathui.ts | 22 +- packages/ckeditor5-math/theme/mathform.css | 201 ++++++++++-------- packages/ckeditor5-math/vitest.config.ts | 3 + 10 files changed, 378 insertions(+), 363 deletions(-) create mode 100644 packages/ckeditor5-math/src/ui/mathinputview.ts delete mode 100644 packages/ckeditor5-math/src/ui/mathliveinputview.ts delete mode 100644 packages/ckeditor5-math/src/ui/rawlatexinputview.ts diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index a27ee87fd..5f36396d1 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -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'; diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index d7ceb2f20..5e9d069cf 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -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; } diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts new file mode 100644 index 000000000..72c4e08fe --- /dev/null +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -0,0 +1,198 @@ +import { View, type Locale } from 'ckeditor5'; + +interface MathFieldElement extends HTMLElement { + value: string; + readOnly: boolean; + mathVirtualKeyboardPolicy: string; + inlineShortcuts?: Record; + 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 { + 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(); + } +} diff --git a/packages/ckeditor5-math/src/ui/mathliveinputview.ts b/packages/ckeditor5-math/src/ui/mathliveinputview.ts deleted file mode 100644 index a2d540228..000000000 --- a/packages/ckeditor5-math/src/ui/mathliveinputview.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { View, type Locale } from 'ckeditor5'; - -interface MathFieldElement extends HTMLElement { - value: string; - readOnly: boolean; - mathVirtualKeyboardPolicy: string; - inlineShortcuts?: Record; -} - -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 { - 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(); - } -} diff --git a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts b/packages/ckeditor5-math/src/ui/rawlatexinputview.ts deleted file mode 100644 index cc468b434..000000000 --- a/packages/ckeditor5-math/src/ui/rawlatexinputview.ts +++ /dev/null @@ -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 { - /** - * 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(); - } -} diff --git a/packages/ckeditor5-math/tests/index.ts b/packages/ckeditor5-math/tests/index.ts index 4493420e1..b379ad8c6 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 573984368..173fae184 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(); } ); diff --git a/packages/ckeditor5-math/tests/mathui.ts b/packages/ckeditor5-math/tests/mathui.ts index 6317b5e66..94becd000 100644 --- a/packages/ckeditor5-math/tests/mathui.ts +++ b/packages/ckeditor5-math/tests/mathui.ts @@ -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, 'f[o]o' ); } ); - 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', () => { diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 1465b9e1b..0d401fa5e 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -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; +} diff --git a/packages/ckeditor5-math/vitest.config.ts b/packages/ckeditor5-math/vitest.config.ts index fb7ccfff0..47a35ced8 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: { From 8a385972fc36cb6f9d1e2f292ff68f0589655657 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Mon, 8 Dec 2025 18:49:06 +0100 Subject: [PATCH 31/67] Close Virtual Keyboard when Mathinput is closed --- packages/ckeditor5-math/src/mathui.ts | 3 +++ packages/ckeditor5-math/src/ui/mainformview.ts | 4 ++++ packages/ckeditor5-math/src/ui/mathinputview.ts | 14 +++++++------- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index 5f36396d1..4bba3f304 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -189,6 +189,9 @@ export default class MathUI extends Plugin { private _removeFormView() { if ( this._isFormInPanel && this.formView ) { + // Hide virtual keyboard before removing the form + this.formView.hideKeyboard(); + this.formView.saveButtonView.focus(); this._balloon.remove( this.formView ); diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 5e9d069cf..f8d8901cc 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -172,4 +172,8 @@ export default class MainFormView extends View { } ); return btn; } + + public hideKeyboard(): void { + this.mathInputView.hideKeyboard(); + } } diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index 72c4e08fe..c0e78b274 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -36,13 +36,13 @@ export default class MathInputView extends View { tag: 'div', attributes: { class: ['ck-mathlive-container'] } }, - // LaTeX label (outside wrapper) + // LaTeX label { tag: 'label', attributes: { class: ['ck-latex-label'] }, children: [t('LaTeX')] }, - // Raw LaTeX wrapper (just textarea now) + // Raw LaTeX wrapper { tag: 'div', attributes: { class: ['ck-latex-wrapper'] }, @@ -176,15 +176,15 @@ export default class MathInputView extends View { } public hideKeyboard(): void { - if (this.mathfield) { - try { - this.mathfield.blur(); - (this.mathfield as any).executeCommand?.('hideVirtualKeyboard'); - } catch { /* MathLive may already be disposed */ } + if (typeof window !== 'undefined' && window.mathVirtualKeyboard?.visible) { + window.mathVirtualKeyboard.hide(); } } public override destroy(): void { + // Hide keyboard before destroying + this.hideKeyboard(); + if (this.mathfield) { try { this.mathfield.blur(); From f1b2d0b870e7bf1044ac62f562bfa9cd14543054 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Mon, 8 Dec 2025 20:22:52 +0100 Subject: [PATCH 32/67] Increas Mathfield font size and ensure virtual keyboard appears above CKEditor --- packages/ckeditor5-math/theme/mathform.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 0d401fa5e..628105078 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -2,6 +2,11 @@ * Math equation editor dialog styles */ +/* Ensure MathLive virtual keyboard appears above CKEditor balloon */ +.ML__keyboard { + z-index: 10001 !important; +} + /* ============================================================================ Main Dialog Container ========================================================================= */ @@ -92,6 +97,7 @@ width: 100%; max-width: 100%; overflow-x: auto !important; + font-size: 1.5em; /* MathLive theme customization */ --selection-background-color: rgba(33, 150, 243, 0.2); From 70f46de2d88776c2e985b0f7e39474b4405d6e7c Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Mon, 8 Dec 2025 20:30:07 +0100 Subject: [PATCH 33/67] MathLive virtual keyboard only appears when focusing the mathfield --- .../ckeditor5-math/src/ui/mathinputview.ts | 336 +++++++++--------- 1 file changed, 173 insertions(+), 163 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index c0e78b274..31171d012 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -1,198 +1,208 @@ import { View, type Locale } from 'ckeditor5'; interface MathFieldElement extends HTMLElement { - value: string; - readOnly: boolean; - mathVirtualKeyboardPolicy: string; - inlineShortcuts?: Record; - setValue(value: string, options?: { silenceNotifications?: boolean }): void; + value: string; + readOnly: boolean; + mathVirtualKeyboardPolicy: string; + inlineShortcuts?: Record; + 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 declare value: string | null; + public declare isReadOnly: boolean; - public mathfield: MathFieldElement | null = null; - private _textarea: HTMLTextAreaElement | null = null; + public mathfield: MathFieldElement | null = null; + private _textarea: HTMLTextAreaElement | null = null; - constructor(locale: Locale) { - super(locale); - const t = locale.t; + constructor( locale: Locale ) { + super( locale ); + const t = locale.t; - this.set('value', null); - this.set('isReadOnly', false); + 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 - { - tag: 'label', - attributes: { class: ['ck-latex-label'] }, - children: [t('LaTeX')] - }, - // Raw LaTeX wrapper - { - 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' - } - } - ] - } - ] - }); - } + this.setTemplate( { + tag: 'div', + attributes: { + class: [ 'ck', 'ck-math-input' ] + }, + children: [ + // MathLive container + { + tag: 'div', + attributes: { class: [ 'ck-mathlive-container' ] } + }, + // LaTeX label + { + tag: 'label', + attributes: { class: [ 'ck-latex-label' ] }, + children: [ t( 'LaTeX' ) ] + }, + // Raw LaTeX wrapper + { + 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(); + 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._textarea = this.element!.querySelector( '.ck-latex-textarea' ) as HTMLTextAreaElement; + this._textarea.value = this.value ?? ''; + this._textarea.readOnly = this.isReadOnly; - this._loadMathLive(); + 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; - }); + // 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 }); - } - }); + // 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; - }); - } + 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 { - try { - await import('mathlive'); - await customElements.whenDefined('math-field'); + private async _loadMathLive(): Promise { + 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; - } + // 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'; - } - } - } + 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; + 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'; + const mathfield = document.createElement( 'math-field' ) as MathFieldElement; + mathfield.mathVirtualKeyboardPolicy = 'auto'; - // Add common shortcuts - mathfield.addEventListener('mount', () => { - mathfield.inlineShortcuts = { - ...mathfield.inlineShortcuts, - dx: 'dx', - dy: 'dy', - dt: 'dt' - }; - }, { once: true }); + // 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; + // Focus mathfield when virtual keyboard button is clicked + mathfield.addEventListener( 'mount', () => { + const toggleBtn = mathfield.shadowRoot?.querySelector( '[part="virtual-keyboard-toggle"]' ) as HTMLButtonElement; + if ( toggleBtn ) { + toggleBtn.addEventListener( 'click', () => { + mathfield.focus(); + } ); + } + }, { once: true } ); - if (this._textarea && this.value) { - this._textarea.value = this.value; - } + // 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; - // 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 */ } - }); + if ( this._textarea && this.value ) { + this._textarea.value = this.value; + } - // 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 */ } - }); + // 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 */ } + } ); - container.appendChild(mathfield); - this.mathfield = mathfield; - } + // 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 */ } + } ); - public focus(): void { - this.mathfield?.focus(); - } + container.appendChild( mathfield ); + this.mathfield = mathfield; + } - public hideKeyboard(): void { - if (typeof window !== 'undefined' && window.mathVirtualKeyboard?.visible) { - window.mathVirtualKeyboard.hide(); - } - } + public focus(): void { + this.mathfield?.focus(); + } - public override destroy(): void { - // Hide keyboard before destroying - this.hideKeyboard(); + public hideKeyboard(): void { + if ( typeof window !== 'undefined' && window.mathVirtualKeyboard?.visible ) { + window.mathVirtualKeyboard.hide(); + } + } - if (this.mathfield) { - try { - this.mathfield.blur(); - this.mathfield.remove(); - } catch { /* MathLive cleanup error */ } - this.mathfield = null; - } - this._textarea = null; - super.destroy(); - } + public override destroy(): void { + // Hide keyboard before destroying + this.hideKeyboard(); + + if ( this.mathfield ) { + try { + this.mathfield.blur(); + this.mathfield.remove(); + } catch { /* MathLive cleanup error */ } + this.mathfield = null; + } + this._textarea = null; + super.destroy(); + } } From b1d92c4fe6874f154764572c42080157ea9d6e29 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Mon, 8 Dec 2025 22:39:12 +0100 Subject: [PATCH 34/67] Fix Tab issues --- .../ckeditor5-math/src/ui/mainformview.ts | 23 +- .../ckeditor5-math/src/ui/mathinputview.ts | 213 +++++++----------- 2 files changed, 103 insertions(+), 133 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index f8d8901cc..cb96826a0 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -100,20 +100,31 @@ export default class MainFormView extends View { submitHandler( { view: this } ); - // Register focusables - [ - this.mathInputView, + const focusableViews = [ + this.mathInputView.latexTextAreaView, this.displayButtonView, this.saveButtonView, this.cancelButtonView - ].forEach( v => { + ]; + + focusableViews.forEach( v => { + this._focusables.add( v ); if ( v.element ) { - this._focusables.add( v ); this.focusTracker.add( v.element ); } } ); - if ( this.element ) this.keystrokes.listenTo( this.element ); + this.mathInputView.on( 'mathfieldReady', () => { + const mathfieldView = this.mathInputView.mathFieldFocusableView; + if ( mathfieldView.element ) { + this._focusables.add( mathfieldView, 0 ); + this.focusTracker.add( mathfieldView.element ); + } + } ); + + if ( this.element ) { + this.keystrokes.listenTo( this.element ); + } } public get equation(): string { diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index 31171d012..7df5c590d 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -1,4 +1,4 @@ -import { View, type Locale } from 'ckeditor5'; +import { View, type Locale, type FocusableView } from 'ckeditor5'; interface MathFieldElement extends HTMLElement { value: string; @@ -8,57 +8,71 @@ interface MathFieldElement extends HTMLElement { setValue( value: string, options?: { silenceNotifications?: boolean } ): void; } -/** - * Combined math input with MathLive visual editor and raw LaTeX textarea. - */ +export class MathFieldFocusableView extends View implements FocusableView { + public declare element: HTMLElement | null; + private _mathInputView: MathInputView; + + constructor( locale: Locale, mathInputView: MathInputView ) { + super( locale ); + this._mathInputView = mathInputView; + } + + public focus(): void { + this._mathInputView.mathfield?.focus(); + } + + public setElement( el: HTMLElement ): void { + ( this as any ).element = el; + } +} + +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' ], + autocapitalize: 'off', + autocomplete: 'off', + autocorrect: 'off', + spellcheck: 'false', + tabindex: 0 + } + } ); + } + + public focus(): void { + this.element?.focus(); + } +} + 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; + public readonly latexTextAreaView: LatexTextAreaView; + public readonly mathFieldFocusableView: MathFieldFocusableView; constructor( locale: Locale ) { super( locale ); const t = locale.t; + 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' ] - }, + attributes: { class: [ 'ck', 'ck-math-input' ] }, children: [ - // MathLive container - { - tag: 'div', - attributes: { class: [ 'ck-mathlive-container' ] } - }, - // LaTeX label - { - tag: 'label', - attributes: { class: [ 'ck-latex-label' ] }, - children: [ t( 'LaTeX' ) ] - }, - // Raw LaTeX wrapper - { - 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' - } - } - ] - } + { tag: 'div', attributes: { class: [ 'ck-mathlive-container' ] } }, + { tag: 'label', attributes: { class: [ 'ck-latex-label' ] }, children: [ t( 'LaTeX' ) ] }, + { tag: 'div', attributes: { class: [ 'ck-latex-wrapper' ] }, children: [ this.latexTextAreaView ] } ] } ); } @@ -66,36 +80,24 @@ export default class MathInputView extends View { 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; + const textarea = this.latexTextAreaView.element; + textarea.value = this.value ?? ''; + textarea.readOnly = this.isReadOnly; - this._loadMathLive(); - - // Textarea -> observable (and sync to mathfield) - this._textarea.addEventListener( 'input', () => { - const val = this._textarea!.value; + textarea.addEventListener( 'input', () => { + const val = textarea.value; if ( this.mathfield ) { this.mathfield.setValue( val, { silenceNotifications: true } ); } - this.value = val.length ? val : null; + this.value = 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', ( _e, _n, val ) => { + textarea.readOnly = val; + if ( this.mathfield ) { this.mathfield.readOnly = val; } } ); - this.on( 'change:isReadOnly', ( _evt, _name, newValue ) => { - if ( this._textarea ) { this._textarea.readOnly = newValue; } - if ( this.mathfield ) { this.mathfield.readOnly = newValue; } - } ); + this._loadMathLive(); } private async _loadMathLive(): Promise { @@ -103,21 +105,17 @@ export default class MathInputView extends View { 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'; - } + if ( this.element ) { this._createMathField(); } + } catch ( e ) { + console.error( 'MathLive load failed:', e ); + const c = this.element?.querySelector( '.ck-mathlive-container' ); + if ( c ) { c.textContent = 'Math editor unavailable'; } } } @@ -125,60 +123,28 @@ export default class MathInputView extends View { const container = this.element?.querySelector( '.ck-mathlive-container' ); if ( !container ) { return; } - const mathfield = document.createElement( 'math-field' ) as MathFieldElement; - mathfield.mathVirtualKeyboardPolicy = 'auto'; + const mf = document.createElement( 'math-field' ) as MathFieldElement; + mf.mathVirtualKeyboardPolicy = 'auto'; + mf.setAttribute( 'tabindex', '-1' ); + mf.value = this.value ?? ''; + mf.readOnly = this.isReadOnly; - // Add common shortcuts - mathfield.addEventListener( 'mount', () => { - mathfield.inlineShortcuts = { - ...mathfield.inlineShortcuts, - dx: 'dx', - dy: 'dy', - dt: 'dt' - }; + mf.addEventListener( 'mount', () => { + mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' }; + const btn = mf.shadowRoot?.querySelector( '[part="virtual-keyboard-toggle"]' ) as HTMLElement; + btn?.addEventListener( 'click', () => mf.focus() ); }, { once: true } ); - // Focus mathfield when virtual keyboard button is clicked - mathfield.addEventListener( 'mount', () => { - const toggleBtn = mathfield.shadowRoot?.querySelector( '[part="virtual-keyboard-toggle"]' ) as HTMLButtonElement; - if ( toggleBtn ) { - toggleBtn.addEventListener( 'click', () => { - mathfield.focus(); - } ); - } - }, { 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 */ } + mf.addEventListener( 'input', () => { + const val = mf.value; + this.latexTextAreaView.element.value = val; + this.value = val || null; } ); - // 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; + container.appendChild( mf ); + this.mathfield = mf; + this.mathFieldFocusableView.setElement( mf ); + this.fire( 'mathfieldReady' ); } public focus(): void { @@ -186,23 +152,16 @@ export default class MathInputView extends View { } public hideKeyboard(): void { - if ( typeof window !== 'undefined' && window.mathVirtualKeyboard?.visible ) { - window.mathVirtualKeyboard.hide(); - } + const vk = ( window as any ).mathVirtualKeyboard; + if ( vk?.visible ) { vk.hide(); } } public override destroy(): void { - // Hide keyboard before destroying this.hideKeyboard(); - if ( this.mathfield ) { - try { - this.mathfield.blur(); - this.mathfield.remove(); - } catch { /* MathLive cleanup error */ } + try { this.mathfield.blur(); this.mathfield.remove(); } catch { /* ignore */ } this.mathfield = null; } - this._textarea = null; super.destroy(); } } From 30ea81d0fbb2c88bdc40acf4da64a072fe2ad42f Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Mon, 8 Dec 2025 22:59:08 +0100 Subject: [PATCH 35/67] Improve virtual keyboard logic and fix Tab issues --- .../ckeditor5-math/src/ui/mainformview.ts | 28 +++++------ .../ckeditor5-math/src/ui/mathinputview.ts | 49 ++++++++++++++++--- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index cb96826a0..2c327517c 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -134,42 +134,42 @@ export default class MainFormView extends View { public set equation( equation: string ) { const norm = equation.trim(); this.mathInputView.value = norm.length ? norm : null; - if ( this.mathView ) this.mathView.value = norm; + if ( this.mathView ) { + this.mathView.value = norm; + } } public focus(): void { this._focusCycler.focusFirst(); } - /** Handle delimiter stripping and preview updates. */ - private _setupSync(previewEnabled: boolean): void { - this.mathInputView.on('change:value', () => { - let eq = (this.mathInputView.value ?? '').trim(); + 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)) { - const params = extractDelimiters(eq); + if ( hasDelimiters( eq ) ) { + const params = extractDelimiters( eq ); eq = params.equation; this.displayButtonView.isOn = params.display; - // Update the input with stripped delimiters - if (this.mathInputView.value !== eq) { + if ( this.mathInputView.value !== eq ) { this.mathInputView.value = eq.length ? eq : null; } } - // Update preview - if (previewEnabled && this.mathView && this.mathView.value !== eq) { + if ( previewEnabled && this.mathView && this.mathView.value !== eq ) { this.mathView.value = eq; } - }); + } ); } private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView { const btn = new ButtonView( this.locale ); btn.set( { label, icon, tooltip: true } ); btn.extendTemplate( { attributes: { class: className } } ); - if (type) btn.type = type; + if ( type ) { + btn.type = type; + } return btn; } diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index 7df5c590d..b296c86aa 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -84,6 +84,11 @@ export default class MathInputView extends View { textarea.value = this.value ?? ''; textarea.readOnly = this.isReadOnly; + if ( this.mathfield ) { + this.mathfield.remove(); + this.mathfield = null; + } + textarea.addEventListener( 'input', () => { const val = textarea.value; if ( this.mathfield ) { @@ -92,11 +97,30 @@ export default class MathInputView extends View { this.value = val || null; } ); + this.on( 'change:value', ( _e, _n, val ) => { + const newVal = val ?? ''; + textarea.value = newVal; + if ( this.mathfield && this.mathfield.value !== newVal ) { + this.mathfield.setValue( newVal, { silenceNotifications: true } ); + } + } ); + this.on( 'change:isReadOnly', ( _e, _n, val ) => { textarea.readOnly = val; - if ( this.mathfield ) { this.mathfield.readOnly = val; } + if ( this.mathfield ) { + this.mathfield.readOnly = val; + } } ); + const vk = ( window as any ).mathVirtualKeyboard; + if ( vk ) { + vk.addEventListener( 'geometrychange', () => { + if ( vk.visible && document.activeElement === textarea && this.mathfield ) { + this.mathfield.focus(); + } + } ); + } + this._loadMathLive(); } @@ -111,17 +135,23 @@ export default class MathInputView extends View { MathfieldClass.plonkSound = null; } - if ( this.element ) { this._createMathField(); } + if ( this.element ) { + this._createMathField(); + } } catch ( e ) { console.error( 'MathLive load failed:', e ); const c = this.element?.querySelector( '.ck-mathlive-container' ); - if ( c ) { c.textContent = 'Math editor unavailable'; } + if ( c ) { + c.textContent = 'Math editor unavailable'; + } } } private _createMathField(): void { const container = this.element?.querySelector( '.ck-mathlive-container' ); - if ( !container ) { return; } + if ( !container ) { + return; + } const mf = document.createElement( 'math-field' ) as MathFieldElement; mf.mathVirtualKeyboardPolicy = 'auto'; @@ -131,8 +161,6 @@ export default class MathInputView extends View { mf.addEventListener( 'mount', () => { mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' }; - const btn = mf.shadowRoot?.querySelector( '[part="virtual-keyboard-toggle"]' ) as HTMLElement; - btn?.addEventListener( 'click', () => mf.focus() ); }, { once: true } ); mf.addEventListener( 'input', () => { @@ -153,13 +181,18 @@ export default class MathInputView extends View { public hideKeyboard(): void { const vk = ( window as any ).mathVirtualKeyboard; - if ( vk?.visible ) { vk.hide(); } + if ( vk?.visible ) { + vk.hide(); + } } public override destroy(): void { this.hideKeyboard(); if ( this.mathfield ) { - try { this.mathfield.blur(); this.mathfield.remove(); } catch { /* ignore */ } + try { + this.mathfield.blur(); + this.mathfield.remove(); + } catch { /* ignore */ } this.mathfield = null; } super.destroy(); From 60debca37bf3fa096eec886f039100416d6ac85c Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Wed, 10 Dec 2025 18:36:34 +0100 Subject: [PATCH 36/67] Improve comments --- packages/ckeditor5-math/src/mathui.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index 4bba3f304..8b41510e0 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -33,8 +33,10 @@ 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 ); @@ -102,21 +104,24 @@ 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(); } ); - // Enter to submit, Shift+Enter for newline + // 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'); @@ -164,6 +169,9 @@ export default class MathUI extends Plugin { this.formView.displayButtonView.isOn = mathCommand.display || false; } + /** + * @private + */ public _hideUI(): void { if ( !this._isFormInPanel ) { return; @@ -175,6 +183,8 @@ 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(); } @@ -195,6 +205,7 @@ export default class MathUI extends Plugin { this.formView.saveButtonView.focus(); this._balloon.remove( this.formView ); + // Hide preview element const previewEl = document.getElementById( this._previewUid ); if ( previewEl ) { previewEl.style.visibility = 'hidden'; From 29f0881c5a797cb178c5943d65f4a81a629b4352 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Wed, 10 Dec 2025 22:44:02 +0100 Subject: [PATCH 37/67] Fix clicking issue in Mathfield --- packages/ckeditor5-math/src/mathui.ts | 1 - .../ckeditor5-math/src/ui/mainformview.ts | 2 +- .../ckeditor5-math/src/ui/mathinputview.ts | 2 +- packages/ckeditor5-math/theme/mathform.css | 54 +++++++++---------- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index 8b41510e0..030e34e4d 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -71,7 +71,6 @@ export default class MathUI extends Plugin { throw new CKEditorError( 'math-command' ); } - const mathConfig = editor.config.get( 'math' )!; const formView = new MainFormView( diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 2c327517c..8e7d62ed4 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -7,7 +7,7 @@ import { View, ViewCollection, type FocusableView, - Locale, + type Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5'; diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index b296c86aa..a5531fca8 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -160,7 +160,7 @@ export default class MathInputView extends View { mf.readOnly = this.isReadOnly; mf.addEventListener( 'mount', () => { - mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' }; + mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: '', dy: '', dt: '' }; }, { once: true } ); mf.addEventListener( 'input', () => { diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 628105078..585b0ba70 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -2,9 +2,18 @@ * Math equation editor dialog styles */ -/* Ensure MathLive virtual keyboard appears above CKEditor balloon */ -.ML__keyboard { - z-index: 10001 !important; +/* Ensure MathLive UI (keyboard, popovers, autocomplete) appears above CKEditor balloon */ +.ML__keyboard, +.ML__popover, +.ML__menu, +.ML__suggestions, +.ML__autocomplete, +[data-ml-root], +/* MathLive LaTeX suggestions popover (by id, from DOM) */ +#mathlive-suggestion-popover, +/* Potential class name variant for suggestions popover */ +.mathlive-suggestions-popover { + z-index: 100001 !important; } /* ============================================================================ @@ -18,7 +27,10 @@ box-sizing: border-box; max-width: 80vw; max-height: 80vh; - overflow: hidden; + overflow-x: hidden; + /* Allow text/MathLive selection inside the dialog even though + the global body style sets user-select: none; */ + user-select: text; } /* Mobile responsiveness */ @@ -32,6 +44,12 @@ Content Layout ========================================================================= */ +.ck-math-scroll { + overflow: auto; + flex: 1 1 auto; + min-height: 0; +} + .ck-math-view { display: flex; flex-direction: column; @@ -40,7 +58,6 @@ min-height: 0; min-width: 0; width: 100%; - overflow: hidden; } .ck-math-button-row { @@ -61,7 +78,8 @@ .ck.ck-math-form math-field, .ck.ck-math-form textarea { box-sizing: border-box; - font-size: var(--ck-font-size-base); + padding: var(--ck-spacing-small); + background: var(--ck-color-input-background) !important; color: var(--ck-color-input-text, inherit); background: transparent !important; border: none !important; @@ -106,22 +124,10 @@ } /* ============================================================================ - MathLive Configuration + MathLive Visual Editor ========================================================================= */ -/* Configure MathLive shadow DOM layout */ -.ck.ck-math-form math-field::part(container), -.ck.ck-math-form math-field::part(content), -.ck.ck-math-form math-field::part(field) { - display: flex; - flex-direction: column; - flex: 1 1 auto; - height: 100%; - align-items: flex-start; - justify-content: flex-start; -} - -/* Position MathLive UI controls */ +/* Position MathLive UI controls (keep default internal layout and sizing) */ .ck.ck-math-form math-field::part(virtual-keyboard-toggle), .ck.ck-math-form math-field::part(menu-toggle) { position: absolute; @@ -149,7 +155,6 @@ width: 100%; flex: 1 1 auto; min-height: 0; - overflow: hidden; } .ck.ck-math-input .ck-mathlive-container { @@ -207,15 +212,10 @@ background: transparent !important; border: none !important; padding: 0; - box-shadow: none !important; - transition: none !important; - animation: none !important; } .ck.ck-math-input .ck-latex-textarea:focus { outline: none; - box-shadow: none !important; - transition: none !important; } /* ============================================================================ @@ -233,7 +233,7 @@ } /* ============================================================================ - Preview Section (always at bottom) + Preview Section ========================================================================= */ .ck-math-preview { From 633a09d414dc61751408ec8aa80091bd82fefad2 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Thu, 11 Dec 2025 23:06:13 +0100 Subject: [PATCH 38/67] Fix sync bug --- packages/ckeditor5-math/src/ui/mainformview.ts | 2 +- .../ckeditor5-math/src/ui/mathinputview.ts | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 8e7d62ed4..1c2403e18 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -116,7 +116,7 @@ export default class MainFormView extends View { this.mathInputView.on( 'mathfieldReady', () => { const mathfieldView = this.mathInputView.mathFieldFocusableView; - if ( mathfieldView.element ) { + if ( mathfieldView.element && !this._focusables.has( mathfieldView ) ) { this._focusables.add( mathfieldView, 0 ); this.focusTracker.add( mathfieldView.element ); } diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index a5531fca8..945bebc79 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -91,13 +91,24 @@ export default class MathInputView extends View { textarea.addEventListener( 'input', () => { const val = textarea.value; - if ( this.mathfield ) { - this.mathfield.setValue( val, { silenceNotifications: true } ); - } this.value = val || null; + if ( this.mathfield ) { + if ( val === '' ) { + this.mathfield.remove(); + this.mathfield = null; + this._createMathField(); + } else { + this.mathfield.setValue( val, { silenceNotifications: true } ); + } + } + this._isSyncing = false; } ); this.on( 'change:value', ( _e, _n, val ) => { + if ( this._isSyncing ) { + return; + } + this._isSyncing = true; const newVal = val ?? ''; textarea.value = newVal; if ( this.mathfield && this.mathfield.value !== newVal ) { @@ -167,6 +178,7 @@ export default class MathInputView extends View { const val = mf.value; this.latexTextAreaView.element.value = val; this.value = val || null; + this._isSyncing = false; } ); container.appendChild( mf ); From 22941a9ce0951de5e0682d2cc61ecbdd9bfd5cd9 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Fri, 12 Dec 2025 19:48:09 +0100 Subject: [PATCH 39/67] Fix sync issues --- packages/ckeditor5-math/src/ui/mainformview.ts | 13 ++++++++----- .../ckeditor5-math/src/ui/mathinputview.ts | 6 ++++++ packages/ckeditor5-math/src/ui/mathview.ts | 18 +++++++++--------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 1c2403e18..d459354fb 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -11,8 +11,8 @@ import { FocusTracker, KeystrokeHandler } 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 { IconCheck } from 'ckeditor5'; +import { IconCancel } from 'ckeditor5'; import { extractDelimiters, hasDelimiters } from '../utils.js'; import MathView, { type MathViewOptions } from './mathview.js'; import MathInputView from './mathinputview.js'; @@ -35,7 +35,7 @@ export default class MainFormView extends View { locale: Locale, mathViewOptions: MathViewOptions, previewEnabled = false, - popupClassName: string[] = [] + popupClassName: Array = [] ) { super( locale ); const t = locale.t; @@ -49,7 +49,7 @@ export default class MainFormView extends View { // Build children - const children: View[] = [ + const children: Array = [ this.mathInputView, this.displayButtonView ]; @@ -116,7 +116,10 @@ export default class MainFormView extends View { this.mathInputView.on( 'mathfieldReady', () => { const mathfieldView = this.mathInputView.mathFieldFocusableView; - if ( mathfieldView.element && !this._focusables.has( mathfieldView ) ) { + if ( mathfieldView.element ) { + if ( this._focusables.has( mathfieldView ) ) { + this._focusables.remove( mathfieldView ); + } this._focusables.add( mathfieldView, 0 ); this.focusTracker.add( mathfieldView.element ); } diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index 945bebc79..ace20848c 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -55,6 +55,7 @@ export default class MathInputView extends View { public mathfield: MathFieldElement | null = null; public readonly latexTextAreaView: LatexTextAreaView; public readonly mathFieldFocusableView: MathFieldFocusableView; + private _isSyncing = false; constructor( locale: Locale ) { super( locale ); @@ -185,6 +186,11 @@ export default class MathInputView extends View { this.mathfield = mf; this.mathFieldFocusableView.setElement( mf ); this.fire( 'mathfieldReady' ); + + // Auto-focus the mathfield when it's ready + setTimeout( () => { + mf.focus(); + }, 0 ); } public focus(): void { diff --git a/packages/ckeditor5-math/src/ui/mathview.ts b/packages/ckeditor5-math/src/ui/mathview.ts index 87af15d08..aa1027329 100644 --- a/packages/ckeditor5-math/src/ui/mathview.ts +++ b/packages/ckeditor5-math/src/ui/mathview.ts @@ -55,20 +55,20 @@ export default class MathView extends View { } public updateMath(): void { - if (!this.element) { + if ( !this.element ) { return; } // Handle empty equations - if (!this.value || !this.value.trim()) { + if ( !this.value || !this.value.trim() ) { this.element.textContent = ''; - this.element.classList.remove('ck-math-render-error'); + this.element.classList.remove( 'ck-math-render-error' ); return; } // Clear previous render this.element.textContent = ''; - this.element.classList.remove('ck-math-render-error'); + this.element.classList.remove( 'ck-math-render-error' ); renderEquation( this.value, @@ -80,14 +80,14 @@ export default class MathView extends View { this.options.previewUid, this.options.previewClassName, this.options.katexRenderOptions - ).catch(error => { - console.error('Math rendering failed:', error); + ).catch( error => { + console.error( 'Math rendering failed:', error ); - if (this.element) { + if ( this.element ) { this.element.textContent = 'Error rendering equation'; - this.element.classList.add('ck-math-render-error'); + this.element.classList.add( 'ck-math-render-error' ); } - }); + } ); } public override render(): void { From a6ede8f9059204a375dd2d42c5bc22eca5328ae8 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Fri, 12 Dec 2025 21:33:59 +0100 Subject: [PATCH 40/67] Improve mathinputview --- .../ckeditor5-math/src/ui/mathinputview.ts | 208 +++++++++--------- 1 file changed, 107 insertions(+), 101 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index ace20848c..f14a5c9a8 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -1,218 +1,224 @@ +// 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'; - +// Narrow interface for the MathLive element we care about. interface MathFieldElement extends HTMLElement { value: string; readOnly: boolean; mathVirtualKeyboardPolicy: string; inlineShortcuts?: Record; - setValue( value: string, options?: { silenceNotifications?: boolean } ): void; + setValue?: ( value: string, options?: { silenceNotifications?: boolean } ) => void; } - +// Small wrapper so the math-field can participate in CKEditor focus cycling. export class MathFieldFocusableView extends View implements FocusableView { public declare element: HTMLElement | null; - private _mathInputView: MathInputView; - - constructor( locale: Locale, mathInputView: MathInputView ) { + private _view: MathInputView; + constructor( locale: Locale, view: MathInputView ) { super( locale ); - this._mathInputView = mathInputView; + this._view = view; } - public focus(): void { - this._mathInputView.mathfield?.focus(); + this._view.mathfield?.focus(); } - public setElement( el: HTMLElement ): void { ( this as any ).element = el; } } - +// Simple textarea used to edit the raw LaTeX source. 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' ], - autocapitalize: 'off', - autocomplete: 'off', - autocorrect: 'off', - spellcheck: 'false', - tabindex: 0 - } - } ); + this.setTemplate( { tag: 'textarea', attributes: { + class: [ 'ck', 'ck-textarea', 'ck-latex-textarea' ], spellcheck: 'false', tabindex: 0 + } } ); } - public focus(): void { this.element?.focus(); } } - +// Main view used by the math dialog. 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 _isSyncing = false; + private _destroyed = false; + private _vkGeometryHandler?: () => void; constructor( locale: Locale ) { super( locale ); - const t = locale.t; - 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' ] }, + tag: 'div', attributes: { class: [ 'ck', 'ck-math-input' ] }, children: [ { tag: 'div', attributes: { class: [ 'ck-mathlive-container' ] } }, - { tag: 'label', attributes: { class: [ 'ck-latex-label' ] }, children: [ t( 'LaTeX' ) ] }, + { 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; - textarea.value = this.value ?? ''; - textarea.readOnly = this.isReadOnly; - - if ( this.mathfield ) { - this.mathfield.remove(); - this.mathfield = null; - } - + // Keep value -> textarea -> mathfield in sync when user types LaTeX. textarea.addEventListener( 'input', () => { const val = textarea.value; this.value = val || null; if ( this.mathfield ) { + // When cleared, recreate mathfield to avoid "ghost braces" artifacts. if ( val === '' ) { this.mathfield.remove(); this.mathfield = null; - this._createMathField(); - } else { - this.mathfield.setValue( val, { silenceNotifications: true } ); + this._initMathField( false ); + } else if ( this.mathfield.value !== val ) { + this._setMathfieldValue( val ); } } - this._isSyncing = false; } ); - + // External changes to value (e.g. dialog model) update both views. this.on( 'change:value', ( _e, _n, val ) => { - if ( this._isSyncing ) { - return; - } - this._isSyncing = true; const newVal = val ?? ''; - textarea.value = newVal; - if ( this.mathfield && this.mathfield.value !== newVal ) { - this.mathfield.setValue( newVal, { silenceNotifications: true } ); + if ( textarea.value !== newVal ) { + textarea.value = newVal; + } + if ( this.mathfield ) { + if ( this.mathfield.value !== newVal ) { + this._setMathfieldValue( newVal ); + } + } else if ( newVal !== '' ) { + this._initMathField( false ); } } ); - + // Keep read-only state of both widgets in sync. this.on( 'change:isReadOnly', ( _e, _n, val ) => { textarea.readOnly = val; if ( this.mathfield ) { this.mathfield.readOnly = val; } } ); - const vk = ( window as any ).mathVirtualKeyboard; - if ( vk ) { - vk.addEventListener( 'geometrychange', () => { - if ( vk.visible && document.activeElement === textarea && this.mathfield ) { + if ( vk && !this._vkGeometryHandler ) { + // When the on-screen keyboard appears, push focus back into mathfield + // so typing continues there instead of the textarea/body. + this._vkGeometryHandler = () => { + if ( !vk.visible || !this.mathfield ) { + return; + } + const active = document.activeElement; + if ( active === document.body || active === textarea ) { this.mathfield.focus(); } - } ); + }; + vk.addEventListener( 'geometrychange', this._vkGeometryHandler ); + } + // On first render, reflect initial value into the LaTeX textarea. + const initial = this.value ?? ''; + if ( textarea.value !== initial ) { + textarea.value = initial; } - this._loadMathLive(); } - private async _loadMathLive(): Promise { try { await import( 'mathlive' ); await customElements.whenDefined( 'math-field' ); - + if ( this._destroyed ) { + return; + } const MathfieldClass = customElements.get( 'math-field' ) as any; if ( MathfieldClass ) { + // Disable MathLive sounds globally for a quieter UI. MathfieldClass.soundsDirectory = null; MathfieldClass.plonkSound = null; } - - if ( this.element ) { - this._createMathField(); + if ( this.element && !this._destroyed ) { + this._initMathField( true ); } } catch ( e ) { - console.error( 'MathLive load failed:', e ); + console.error( 'MathLive load error', e ); const c = this.element?.querySelector( '.ck-mathlive-container' ); if ( c ) { c.textContent = 'Math editor unavailable'; } } } - - private _createMathField(): void { + 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', '-1' ); + mf.setAttribute( 'tabindex', '0' ); mf.value = this.value ?? ''; mf.readOnly = this.isReadOnly; - - mf.addEventListener( 'mount', () => { - mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: '', dy: '', dt: '' }; - }, { once: true } ); + container.appendChild( mf ); + try { + const anyMf = mf as any; + // Override only dt/dx/dy, keep other built‑in shortcuts (e.g. frac). + anyMf.inlineShortcuts = { ...( anyMf.inlineShortcuts || {} ), dx: 'dx', dy: 'dy', dt: 'dt' }; + } catch { /* */ } + mf.addEventListener( 'keydown', ev => { + // Let Tab move focus from mathfield into the LaTeX textarea + // instead of being consumed by MathLive. + if ( ev.key === 'Tab' && !ev.shiftKey ) { + ev.preventDefault(); + ev.stopImmediatePropagation(); + this.latexTextAreaView.focus(); + } + }, { capture: true } ); mf.addEventListener( 'input', () => { - const val = mf.value; - this.latexTextAreaView.element.value = val; - this.value = val || null; - this._isSyncing = false; + if ( this.latexTextAreaView.element.value !== mf.value ) { + this.latexTextAreaView.element.value = mf.value; + } + this.value = mf.value || null; } ); - container.appendChild( mf ); this.mathfield = mf; this.mathFieldFocusableView.setElement( mf ); this.fire( 'mathfieldReady' ); - - // Auto-focus the mathfield when it's ready - setTimeout( () => { - mf.focus(); - }, 0 ); + if ( shouldFocus ) { + setTimeout( () => mf.focus(), 0 ); + } } + private _setMathfieldValue( value: string ): void { + const mf = this.mathfield; + if ( !mf ) { + return; + } + if ( mf.setValue ) { + mf.setValue( value, { silenceNotifications: true } ); + } else { + mf.value = value; + } + } + public hideKeyboard(): void { + ( window as any ).mathVirtualKeyboard?.hide(); + } public focus(): void { this.mathfield?.focus(); } - - public hideKeyboard(): void { - const vk = ( window as any ).mathVirtualKeyboard; - if ( vk?.visible ) { - vk.hide(); - } - } - public override destroy(): void { - this.hideKeyboard(); - if ( this.mathfield ) { - try { - this.mathfield.blur(); - this.mathfield.remove(); - } catch { /* ignore */ } - this.mathfield = null; + this._destroyed = true; + const vk = ( window as any ).mathVirtualKeyboard; + if ( vk && this._vkGeometryHandler ) { + vk.removeEventListener( 'geometrychange', this._vkGeometryHandler ); + this._vkGeometryHandler = undefined; } + this.hideKeyboard(); + this.mathfield?.remove(); + this.mathfield = null; super.destroy(); } } From c02491d2e69441704a3c14f389ccc945b21b4896 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Fri, 12 Dec 2025 23:09:20 +0100 Subject: [PATCH 41/67] Remove unnecessary any casts in math plugin --- .../ckeditor5-math/src/ui/mathinputview.ts | 38 ++++++++++++------- packages/ckeditor5-math/theme/mathform.css | 2 +- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index f14a5c9a8..a1285f991 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -1,6 +1,19 @@ // 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'; + +// Type-safe interface for MathLive's global virtual keyboard. +declare global { + interface Window { + mathVirtualKeyboard?: { + visible: boolean; + show: () => void; + hide: () => void; + addEventListener: ( event: string, cb: () => void ) => void; + removeEventListener: ( event: string, cb: () => void ) => void; + }; + } +} // Narrow interface for the MathLive element we care about. interface MathFieldElement extends HTMLElement { value: string; @@ -75,7 +88,7 @@ export default class MathInputView extends View { this.mathfield.remove(); this.mathfield = null; this._initMathField( false ); - } else if ( this.mathfield.value !== val ) { + } else if ( this.mathfield.value.trim() !== val.trim() ) { this._setMathfieldValue( val ); } } @@ -87,7 +100,7 @@ export default class MathInputView extends View { textarea.value = newVal; } if ( this.mathfield ) { - if ( this.mathfield.value !== newVal ) { + if ( this.mathfield.value.trim() !== newVal.trim() ) { this._setMathfieldValue( newVal ); } } else if ( newVal !== '' ) { @@ -101,18 +114,15 @@ export default class MathInputView extends View { this.mathfield.readOnly = val; } } ); - const vk = ( window as any ).mathVirtualKeyboard; + const vk = window.mathVirtualKeyboard; if ( vk && !this._vkGeometryHandler ) { - // When the on-screen keyboard appears, push focus back into mathfield - // so typing continues there instead of the textarea/body. + // When the on-screen keyboard appears, ensure mathfield has focus + // so MathLive captures the keyboard input correctly. this._vkGeometryHandler = () => { if ( !vk.visible || !this.mathfield ) { return; } - const active = document.activeElement; - if ( active === document.body || active === textarea ) { - this.mathfield.focus(); - } + this.mathfield.focus(); }; vk.addEventListener( 'geometrychange', this._vkGeometryHandler ); } @@ -162,6 +172,8 @@ export default class MathInputView extends View { mf.value = this.value ?? ''; mf.readOnly = this.isReadOnly; container.appendChild( mf ); + // Ensure mathfield is ready immediately for virtual keyboard input + mf.focus(); try { const anyMf = mf as any; // Override only dt/dx/dy, keep other built‑in shortcuts (e.g. frac). @@ -178,7 +190,7 @@ export default class MathInputView extends View { }, { capture: true } ); mf.addEventListener( 'input', () => { - if ( this.latexTextAreaView.element.value !== mf.value ) { + if ( this.latexTextAreaView.element.value.trim() !== mf.value.trim() ) { this.latexTextAreaView.element.value = mf.value; } this.value = mf.value || null; @@ -188,7 +200,7 @@ export default class MathInputView extends View { this.mathFieldFocusableView.setElement( mf ); this.fire( 'mathfieldReady' ); if ( shouldFocus ) { - setTimeout( () => mf.focus(), 0 ); + requestAnimationFrame( () => mf.focus() ); } } @@ -204,14 +216,14 @@ export default class MathInputView extends View { } } public hideKeyboard(): void { - ( window as any ).mathVirtualKeyboard?.hide(); + window.mathVirtualKeyboard?.hide(); } public focus(): void { this.mathfield?.focus(); } public override destroy(): void { this._destroyed = true; - const vk = ( window as any ).mathVirtualKeyboard; + const vk = window.mathVirtualKeyboard; if ( vk && this._vkGeometryHandler ) { vk.removeEventListener( 'geometrychange', this._vkGeometryHandler ); this._vkGeometryHandler = undefined; diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index 585b0ba70..fff2feef4 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -13,7 +13,7 @@ #mathlive-suggestion-popover, /* Potential class name variant for suggestions popover */ .mathlive-suggestions-popover { - z-index: 100001 !important; + z-index: calc(var(--ck-z-panel) + 1000) !important; } /* ============================================================================ From 07de353207506bfcfc0e2629612ff6dfffa43bce Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Sun, 14 Dec 2025 20:21:42 +0100 Subject: [PATCH 42/67] Adding comments and improving code quality in math input views --- packages/ckeditor5-math/src/mathui.ts | 14 +-- .../ckeditor5-math/src/ui/mathinputview.ts | 109 +++++++++++------- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/packages/ckeditor5-math/src/mathui.ts b/packages/ckeditor5-math/src/mathui.ts index 030e34e4d..3e3da2744 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(() => { + requestAnimationFrame( () => { this.formView?.mathInputView.focus(); - }); + } ); } private _createFormView() { @@ -90,7 +90,7 @@ export default class MathUI extends Plugin { 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: boolean) => !value ); + formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', ( value: boolean ) => !value ); formView.saveButtonView.bind( 'isEnabled' ).to( mathCommand, 'isEnabled', @@ -121,12 +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(); } - }); + } ); return formView; } diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index a1285f991..65bf31bba 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -2,7 +2,6 @@ // and keeps them in sync for the CKEditor 5 math dialog. import { View, type Locale, type FocusableView } from 'ckeditor5'; -// Type-safe interface for MathLive's global virtual keyboard. declare global { interface Window { mathVirtualKeyboard?: { @@ -14,7 +13,7 @@ declare global { }; } } -// Narrow interface for the MathLive element we care about. + interface MathFieldElement extends HTMLElement { value: string; readOnly: boolean; @@ -22,7 +21,8 @@ interface MathFieldElement extends HTMLElement { inlineShortcuts?: Record; setValue?: ( value: string, options?: { silenceNotifications?: boolean } ) => void; } -// Small wrapper so the math-field can participate in CKEditor focus cycling. + +// 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; @@ -34,10 +34,11 @@ export class MathFieldFocusableView extends View implements FocusableView { this._view.mathfield?.focus(); } public setElement( el: HTMLElement ): void { - ( this as any ).element = el; + this.element = el; } } -// Simple textarea used to edit the raw LaTeX source. + +// 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 ) { @@ -50,7 +51,8 @@ export class LatexTextAreaView extends View implements FocusableView { this.element?.focus(); } } -// Main view used by the math dialog. + +// Main view class for the math input export default class MathInputView extends View { public declare value: string | null; public declare isReadOnly: boolean; @@ -59,6 +61,8 @@ export default class MathInputView extends View { public readonly mathFieldFocusableView: MathFieldFocusableView; private _destroyed = false; private _vkGeometryHandler?: () => void; + private _updating = false; + private static _configured = false; constructor( locale: Locale ) { super( locale ); @@ -75,15 +79,20 @@ export default class MathInputView extends View { ] } ); } + public override render(): void { super.render(); const textarea = this.latexTextAreaView.element; - // Keep value -> textarea -> mathfield in sync when user types LaTeX. - textarea.addEventListener( 'input', () => { + + // 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 ) { - // When cleared, recreate mathfield to avoid "ghost braces" artifacts. if ( val === '' ) { this.mathfield.remove(); this.mathfield = null; @@ -92,9 +101,15 @@ export default class MathInputView extends View { this._setMathfieldValue( val ); } } + this._updating = false; } ); - // External changes to value (e.g. dialog model) update both views. + + // 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; @@ -106,33 +121,36 @@ export default class MathInputView extends View { } else if ( newVal !== '' ) { this._initMathField( false ); } + this._updating = false; } ); - // Keep read-only state of both widgets in sync. + + // 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 ) { - // When the on-screen keyboard appears, ensure mathfield has focus - // so MathLive captures the keyboard input correctly. this._vkGeometryHandler = () => { - if ( !vk.visible || !this.mathfield ) { - return; + if ( vk.visible && this.mathfield ) { + this.mathfield.focus(); } - this.mathfield.focus(); }; vk.addEventListener( 'geometrychange', this._vkGeometryHandler ); } - // On first render, reflect initial value into the LaTeX textarea. + 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' ); @@ -140,23 +158,26 @@ export default class MathInputView extends View { if ( this._destroyed ) { return; } - const MathfieldClass = customElements.get( 'math-field' ) as any; - if ( MathfieldClass ) { - // Disable MathLive sounds globally for a quieter UI. - MathfieldClass.soundsDirectory = null; - MathfieldClass.plonkSound = null; + 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 ( e ) { - console.error( 'MathLive load error', e ); + } 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 ) { @@ -172,30 +193,33 @@ export default class MathInputView extends View { mf.value = this.value ?? ''; mf.readOnly = this.isReadOnly; container.appendChild( mf ); - // Ensure mathfield is ready immediately for virtual keyboard input - mf.focus(); + // Set shortcuts after mounting (accessing inlineShortcuts requires mounted element) try { - const anyMf = mf as any; - // Override only dt/dx/dy, keep other built‑in shortcuts (e.g. frac). - anyMf.inlineShortcuts = { ...( anyMf.inlineShortcuts || {} ), dx: 'dx', dy: 'dy', dt: 'dt' }; - } catch { /* */ } + 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 => { - // Let Tab move focus from mathfield into the LaTeX textarea - // instead of being consumed by MathLive. if ( ev.key === 'Tab' && !ev.shiftKey ) { ev.preventDefault(); ev.stopImmediatePropagation(); this.latexTextAreaView.focus(); } }, { capture: true } ); - mf.addEventListener( 'input', () => { - if ( this.latexTextAreaView.element.value.trim() !== mf.value.trim() ) { - this.latexTextAreaView.element.value = mf.value; + 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' ); @@ -204,23 +228,26 @@ export default class MathInputView extends View { } } + // Updates the mathfield value without triggering loops private _setMathfieldValue( value: string ): void { - const mf = this.mathfield; - if ( !mf ) { + if ( !this.mathfield ) { return; } - if ( mf.setValue ) { - mf.setValue( value, { silenceNotifications: true } ); + if ( this.mathfield.setValue ) { + this.mathfield.setValue( value, { silenceNotifications: true } ); } else { - mf.value = value; + 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; From bc23e0984af0a70c041f9b607e45072061f07f7c Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Sun, 14 Dec 2025 22:00:56 +0100 Subject: [PATCH 43/67] Undo unnecessary formatting changes --- .../ckeditor5-math/src/ui/mainformview.ts | 107 ++++++++++++------ 1 file changed, 74 insertions(+), 33 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index d459354fb..487a96de0 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -1,18 +1,6 @@ -import { - ButtonView, - FocusCycler, - LabelView, - submitHandler, - SwitchButtonView, - View, - ViewCollection, - type FocusableView, - type Locale, - FocusTracker, - KeystrokeHandler -} from 'ckeditor5'; -import { IconCheck } from 'ckeditor5'; -import { IconCancel } 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, { type MathViewOptions } from './mathview.js'; import MathInputView from './mathinputview.js'; @@ -69,20 +57,45 @@ export default class MainFormView extends View { this.setTemplate( { tag: 'form', attributes: { - class: [ 'ck', 'ck-math-form', ...popupClassName ], + 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 } ] + 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 ] + attributes: { + class: [ + 'ck-math-button-row' + ] + }, + children: [ + this.saveButtonView, + this.cancelButtonView + ] } ] } ); @@ -91,14 +104,20 @@ export default class MainFormView extends View { focusables: this._focusables, focusTracker: this.focusTracker, keystrokeHandler: this.keystrokes, - actions: { focusPrevious: 'shift + tab', focusNext: 'tab' } + actions: { + focusPrevious: 'shift + tab', + focusNext: 'tab' + } } ); } public override render(): void { super.render(); - submitHandler( { view: this } ); + // Prevent default form submit event & trigger custom 'submit' + submitHandler( { + view: this + } ); const focusableViews = [ this.mathInputView.latexTextAreaView, @@ -167,24 +186,46 @@ export default class MainFormView extends View { } private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView { - const btn = new ButtonView( this.locale ); - btn.set( { label, icon, tooltip: true } ); - btn.extendTemplate( { attributes: { class: className } } ); + const button = new ButtonView( this.locale ); + + button.set( { + label, + icon, + tooltip: true + } ); + + button.extendTemplate( { + attributes: { + class: className + } + } ); + if ( type ) { - btn.type = type; + button.type = type; } - return btn; + + return button; } private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView { - const btn = new SwitchButtonView( this.locale ); - btn.set( { label: t( 'Display mode' ), withText: true } ); - btn.extendTemplate( { attributes: { class: 'ck-button-display-toggle' } } ); + const switchButton = new SwitchButtonView( this.locale ); - btn.on( 'execute', () => { - btn.isOn = !btn.isOn; + switchButton.set( { + label: t( 'Display mode' ), + withText: true } ); - return btn; + + switchButton.extendTemplate( { + attributes: { + class: 'ck-button-display-toggle' + } + } ); + + switchButton.on( 'execute', () => { + switchButton.isOn = !switchButton.isOn; + } ); + + return switchButton; } public hideKeyboard(): void { From 050ddb8c55ede44661fd5fc1664256c8862c7c02 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Mon, 15 Dec 2025 20:17:58 +0100 Subject: [PATCH 44/67] Improve css to fix tooltips --- .../ckeditor5-math/src/ui/mainformview.ts | 14 +- packages/ckeditor5-math/theme/mathform.css | 313 ++++++++---------- 2 files changed, 147 insertions(+), 180 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mainformview.ts b/packages/ckeditor5-math/src/ui/mainformview.ts index 487a96de0..52d3b0512 100644 --- a/packages/ckeditor5-math/src/ui/mainformview.ts +++ b/packages/ckeditor5-math/src/ui/mainformview.ts @@ -70,20 +70,10 @@ export default class MainFormView extends View { tag: 'div', attributes: { class: [ - 'ck-math-scroll' + 'ck-math-view' ] }, - children: [ - { - tag: 'div', - attributes: { - class: [ - 'ck-math-view' - ] - }, - children - } - ] + children }, { tag: 'div', diff --git a/packages/ckeditor5-math/theme/mathform.css b/packages/ckeditor5-math/theme/mathform.css index fff2feef4..a5d55f2f1 100644 --- a/packages/ckeditor5-math/theme/mathform.css +++ b/packages/ckeditor5-math/theme/mathform.css @@ -1,25 +1,22 @@ /** - * Math equation editor dialog styles + * Math Equation Editor Dialog Styles - Compact & Readable */ -/* Ensure MathLive UI (keyboard, popovers, autocomplete) appears above CKEditor balloon */ -.ML__keyboard, -.ML__popover, -.ML__menu, -.ML__suggestions, -.ML__autocomplete, -[data-ml-root], -/* MathLive LaTeX suggestions popover (by id, from DOM) */ -#mathlive-suggestion-popover, -/* Potential class name variant for suggestions popover */ -.mathlive-suggestions-popover { +/* === 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 Container - ========================================================================= */ - +/* === MAIN DIALOG === */ .ck.ck-math-form { display: flex; flex-direction: column; @@ -27,217 +24,197 @@ box-sizing: border-box; max-width: 80vw; max-height: 80vh; - overflow-x: hidden; - /* Allow text/MathLive selection inside the dialog even though - the global body style sets user-select: none; */ + overflow: visible; user-select: text; } -/* Mobile responsiveness */ -@media screen and (max-width: 600px) { - .ck.ck-math-form { - flex-wrap: wrap; - } -} - -/* ============================================================================ - Content Layout - ========================================================================= */ - -.ck-math-scroll { - overflow: auto; - flex: 1 1 auto; - min-height: 0; -} - +/* 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; - min-width: 0; width: 100%; } -.ck-math-button-row { +/* === MATH INPUT === */ +.ck.ck-math-input { display: flex; - flex-shrink: 0; + flex-direction: column; gap: var(--ck-spacing-standard); - margin-top: var(--ck-spacing-standard); width: fit-content; + min-width: 100%; max-width: 100%; - flex-wrap: wrap; + flex: 1 1 auto; + min-height: 0; + overflow: visible !important; } -/* ============================================================================ - Shared Styles for Input Fields - ========================================================================= */ - -/* Base styling for both MathLive fields and textareas */ -.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); - background: transparent !important; - border: none !important; - 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 */ -.ck.ck-math-form math-field { - display: block !important; +/* === MATHLIVE EDITOR === */ +.ck.ck-math-input .ck-mathlive-container { + position: relative; width: 100%; - max-width: 100%; - overflow-x: auto !important; - font-size: 1.5em; - - /* MathLive theme customization */ - --selection-background-color: rgba(33, 150, 243, 0.2); - --selection-color: inherit; - --contains-highlight-background-color: rgba(0, 0, 0, 0.05); + 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); } -/* ============================================================================ - MathLive Visual Editor - ========================================================================= */ - -/* Position MathLive UI controls (keep default internal layout and sizing) */ -.ck.ck-math-form math-field::part(virtual-keyboard-toggle), -.ck.ck-math-form math-field::part(menu-toggle) { +/* 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.ck-math-form math-field::part(virtual-keyboard-toggle) { - right: 40px; -} - -.ck.ck-math-form math-field::part(menu-toggle) { +.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; } -/* ============================================================================ - Combined Math Input (MathLive + LaTeX Textarea) - ========================================================================= */ - -.ck.ck-math-input { - display: flex; - flex-direction: column; - gap: var(--ck-spacing-standard); +/* Math field element */ +.ck.ck-math-form math-field { + display: block !important; width: 100%; - flex: 1 1 auto; - min-height: 0; -} - -.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); + 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: 100%; - min-height: 60px; - flex: 1 1 auto; + width: fit-content; + min-width: 100%; + max-width: 100%; padding: var(--ck-spacing-small); - border: 1px solid var(--ck-color-input-border, #ccc); - border-radius: var(--ck-border-radius, 6px); + 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, #1a73e8); + border-color: var(--ck-color-focus-border); } - .ck.ck-math-input .ck-latex-label { - display: block; font-size: 12px; font-weight: 600; - color: var(--ck-color-text, #333); + 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 { - display: block; - width: 100%; + width: fit-content; min-width: 100%; + max-width: 100%; min-height: 60px; - max-height: 100%; - flex: 1 1 auto; + max-height: calc(80vh - 300px); resize: both; overflow: auto; - font-family: monospace; + 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; } -.ck.ck-math-input .ck-latex-textarea:focus { - outline: none; +/* === 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); } -/* ============================================================================ - Error State - ========================================================================= */ - -.ck-math-render-error { - color: var(--ck-color-error-text, #db1d1d); +/* === 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); - font-style: italic; - font-size: 0.9em; - border: 1px dashed var(--ck-color-error-text, #db1d1d); - border-radius: 2px; - background: var(--ck-color-base-background, #fff); -} - -/* ============================================================================ - Preview Section - ========================================================================= */ - -.ck-math-preview { + background: transparent !important; + border: none !important; + display: block; + text-align: left; + overflow-x: auto !important; + overflow-y: visible !important; flex-shrink: 0; - overflow-x: auto; - overflow-y: hidden; +} + +/* 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; } From d2391f94c076bf6ecfa906f89f6a3321d850175f Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Mon, 15 Dec 2025 21:32:50 +0100 Subject: [PATCH 45/67] Fix offline math rendering by bundling local fonts --- packages/ckeditor5-math/src/ui/mathinputview.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index 65bf31bba..a3b93f3e1 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -1,6 +1,7 @@ // 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 { From 87ab41c80c05a7ef7a66478de041d3ce6863727d Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Tue, 23 Dec 2025 18:02:40 +0100 Subject: [PATCH 46/67] Fix shift+tab behavior in MathInputView --- packages/ckeditor5-math/src/ui/mathinputview.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-math/src/ui/mathinputview.ts b/packages/ckeditor5-math/src/ui/mathinputview.ts index a3b93f3e1..027b7aab9 100644 --- a/packages/ckeditor5-math/src/ui/mathinputview.ts +++ b/packages/ckeditor5-math/src/ui/mathinputview.ts @@ -203,10 +203,14 @@ export default class MathInputView extends View { // Inline shortcut configuration is optional; ignore failures to avoid breaking the math field. } mf.addEventListener( 'keydown', ev => { - if ( ev.key === 'Tab' && !ev.shiftKey ) { - ev.preventDefault(); - ev.stopImmediatePropagation(); - this.latexTextAreaView.focus(); + if ( ev.key === 'Tab' ) { + if ( ev.shiftKey ) { + ev.preventDefault(); + } else { + ev.preventDefault(); + ev.stopImmediatePropagation(); + this.latexTextAreaView.focus(); + } } }, { capture: true } ); mf.addEventListener( 'input', () => { From f8b5417d6cc99caa645508c5407d857129114ba3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:03:52 +0000 Subject: [PATCH 47/67] chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 --- package.json | 2 +- pnpm-lock.yaml | 26 +++++++++----------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 7ff8c4155..711ebbee5 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/pnpm-lock.yaml b/pnpm-lock.yaml index 31fc4bd62..d2220e08d 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 @@ -12389,8 +12389,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 @@ -12416,8 +12416,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 @@ -15361,8 +15361,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: @@ -15582,8 +15580,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.3.0': dependencies: @@ -15606,8 +15602,6 @@ snapshots: '@ckeditor/ckeditor5-table': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-emoji@47.3.0': dependencies: @@ -16093,8 +16087,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-restricted-editing@47.3.0': dependencies: @@ -28808,7 +28800,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 @@ -28841,9 +28833,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 From f4ccce7de5caae181db9eefb39c83c51b6ab4ca4 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 5 Jan 2026 11:23:50 +0800 Subject: [PATCH 48/67] fix(sql_console): cannot copy table data --- apps/client/src/widgets/sql_result.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/sql_result.tsx b/apps/client/src/widgets/sql_result.tsx index e4fde650b..f5c6b0088 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)) { From 7a6cc4f51ebe674c4df6d304bbde3410afa97394 Mon Sep 17 00:00:00 2001 From: SngAbc <37627919+SiriusXT@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:16:16 +0800 Subject: [PATCH 49/67] fix(sql_console): cannot copy table data Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- apps/client/src/widgets/sql_result.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/sql_result.tsx b/apps/client/src/widgets/sql_result.tsx index f5c6b0088..7aaa5739d 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)) { From 51513d3779bdebd5cb13f9b2e9d6715572d733c1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 5 Jan 2026 21:03:32 +0200 Subject: [PATCH 50/67] fix(status_bar): count not refreshing properly after change --- apps/client/src/widgets/layout/StatusBar.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 3c852ba14..6a021a0b7 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -5,7 +5,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap"; import clsx from "clsx"; import { type ComponentChildren, RefObject } from "preact"; import { createPortal } from "preact/compat"; -import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { CommandNames } from "../../components/app_context"; import NoteContext from "../../components/note_context"; @@ -338,15 +338,19 @@ interface AttributesProps extends StatusBarContext { function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) { const [ count, setCount ] = useState(note.attributes.length); + const refreshCount = useCallback((note: FNote) => { + return note.getAttributes().filter(a => !a.isAutoLink).length; + }, []); + // React to note changes. useEffect(() => { - setCount(note.getAttributes().filter(a => !a.isAutoLink).length); - }, [ note ]); + setCount(refreshCount(note)); + }, [ note, refreshCount ]); // React to changes in count. useTriliumEvent("entitiesReloaded", (({loadResults}) => { if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) { - setCount(note.attributes.length); + setCount(refreshCount(note)); } })); From 494b55d685600a1e1189e2f5ef0d9861d54ff800 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 5 Jan 2026 23:39:36 +0200 Subject: [PATCH 51/67] fix(ckeditor): missing pl locale --- packages/ckeditor5/src/i18n.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ckeditor5/src/i18n.ts b/packages/ckeditor5/src/i18n.ts index a409fa437..dd4e616d3 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"), From 639b1f2863a5489ef2f90943e29fdd49de99ec38 Mon Sep 17 00:00:00 2001 From: Yatrik Patel Date: Mon, 5 Jan 2026 04:00:48 +0100 Subject: [PATCH 52/67] Translated using Weblate (Hindi) Currently translated at 5.9% (9 of 152 strings) Translation: Trilium Notes/Website Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/ --- apps/website/src/translations/hi/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/website/src/translations/hi/translation.json b/apps/website/src/translations/hi/translation.json index f268e30ad..4146c0ca0 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": "वेब क्लिपर" } } From d52b735b99d352340e267f56f6b3560063405d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Zs=C3=B3lya?= Date: Mon, 5 Jan 2026 10:51:33 +0100 Subject: [PATCH 53/67] Translated using Weblate (Hungarian) Currently translated at 1.9% (34 of 1751 strings) Translation: Trilium Notes/Client Translate-URL: https://hosted.weblate.org/projects/trilium/client/hu/ --- apps/client/src/translations/hu/translation.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/client/src/translations/hu/translation.json b/apps/client/src/translations/hu/translation.json index 042fbb281..40e39bec3 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": { From b3c0be7559ede7cfee993af0ceb003492334e047 Mon Sep 17 00:00:00 2001 From: Yatrik Patel Date: Mon, 5 Jan 2026 04:03:15 +0100 Subject: [PATCH 54/67] Translated using Weblate (Hindi) Currently translated at 3.0% (12 of 389 strings) Translation: Trilium Notes/Server Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/ --- apps/server/src/assets/translations/hi/server.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/assets/translations/hi/server.json b/apps/server/src/assets/translations/hi/server.json index 9b03fa2c3..c994dc43d 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": "डुप्लिकेट सबट्री" } } From 5ec521b02482edacf98d49cc0f5d4fe24ce9e96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20N=C3=B8glegaard?= Date: Mon, 5 Jan 2026 22:33:25 +0100 Subject: [PATCH 55/67] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 68.4% (104 of 152 strings) Translation: Trilium Notes/Website Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/ --- .../src/translations/nb-NO/translation.json | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/website/src/translations/nb-NO/translation.json b/apps/website/src/translations/nb-NO/translation.json index f9e3b5a58..9ad41c552 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>notatkart, <1>relasjonskart, <2>lagrede søk, <3>rendret notat, og <4>web view." }, "extensibility_benefits": { "import_export_title": "Import/eksport", From a5841c14230614156b18a70c1f6b49232fb01719 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Wed, 7 Jan 2026 10:11:24 +0800 Subject: [PATCH 56/67] fix(text): Title is not focused when creating a note via the launcher --- apps/client/src/widgets/type_widgets/text/EditableText.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/type_widgets/text/EditableText.tsx b/apps/client/src/widgets/type_widgets/text/EditableText.tsx index e5ea9c498..3148eb8b9 100644 --- a/apps/client/src/widgets/type_widgets/text/EditableText.tsx +++ b/apps/client/src/widgets/type_widgets/text/EditableText.tsx @@ -286,7 +286,7 @@ function useWatchdogCrashHandling() { const currentState = watchdog.state; logInfo(`CKEditor state changed to ${currentState}`); - if (currentState === "ready") { + if (currentState === "ready" && hasCrashed.current === true) { hasCrashed.current = false; watchdog.editor?.focus(); } From fac1f6b16c426591f2c7c18a5cd0a756252fcdf5 Mon Sep 17 00:00:00 2001 From: SngAbc <37627919+SiriusXT@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:33:17 +0800 Subject: [PATCH 57/67] fix(text): Title is not focused when creating a note via the launcher Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- apps/client/src/widgets/type_widgets/text/EditableText.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/type_widgets/text/EditableText.tsx b/apps/client/src/widgets/type_widgets/text/EditableText.tsx index 3148eb8b9..fba7d6966 100644 --- a/apps/client/src/widgets/type_widgets/text/EditableText.tsx +++ b/apps/client/src/widgets/type_widgets/text/EditableText.tsx @@ -286,7 +286,7 @@ function useWatchdogCrashHandling() { const currentState = watchdog.state; logInfo(`CKEditor state changed to ${currentState}`); - if (currentState === "ready" && hasCrashed.current === true) { + if (currentState === "ready" && hasCrashed.current) { hasCrashed.current = false; watchdog.editor?.focus(); } From 263ee864be8df09fea241702875058a1a857c33e Mon Sep 17 00:00:00 2001 From: Yatrik Patel Date: Wed, 7 Jan 2026 00:42:27 +0100 Subject: [PATCH 58/67] Translated using Weblate (Hindi) Currently translated at 9.2% (14 of 152 strings) Translation: Trilium Notes/Website Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/ --- apps/website/src/translations/hi/translation.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/website/src/translations/hi/translation.json b/apps/website/src/translations/hi/translation.json index 4146c0ca0..00e6ce62d 100644 --- a/apps/website/src/translations/hi/translation.json +++ b/apps/website/src/translations/hi/translation.json @@ -15,5 +15,16 @@ "productivity_benefits": { "protected_notes_title": "संरक्षित नोट्स", "web_clipper_title": "वेब क्लिपर" + }, + "note_types": { + "canvas_title": "कैनवास", + "mindmap_title": "माइंडमैप" + }, + "extensibility_benefits": { + "share_title": "वेब पर नोट्स शेयर करें" + }, + "collections": { + "calendar_title": "कैलेंडर", + "table_title": "टेबल" } } From 8f4ebeb335164550c93db6309f57fc029366ac35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20N=C3=B8glegaard?= Date: Tue, 6 Jan 2026 22:33:29 +0100 Subject: [PATCH 59/67] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (152 of 152 strings) Translation: Trilium Notes/Website Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/ --- .../src/translations/nb-NO/translation.json | 78 +++++++++++++++---- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/apps/website/src/translations/nb-NO/translation.json b/apps/website/src/translations/nb-NO/translation.json index 9ad41c552..4da9a79f6 100644 --- a/apps/website/src/translations/nb-NO/translation.json +++ b/apps/website/src/translations/nb-NO/translation.json @@ -51,13 +51,19 @@ "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>notatkart, <1>relasjonskart, <2>lagrede søk, <3>rendret notat, og <4>web view." + "others_list": "og andre: <0>notatkart, <1>relasjonskart, <2>lagrede søk, <3>rendret notat, og <4>web view.", + "canvas_description": "Arranger figurer, bilder og tekst på et uendelig lerret som bruker samme teknologi som excalidraw.com. Ideelt for diagrammer, skisser og visuell planlegging." }, "extensibility_benefits": { "import_export_title": "Import/eksport", "scripting_title": "Avansert skripting", "api_title": "REST API", - "title": "Deling og utvidbarhet" + "title": "Deling og utvidbarhet", + "share_title": "Del notater på nett", + "share_description": "Hvis du har en server, kan den brukes til å dele valgfrie notater med andre.", + "scripting_description": "Lag dine egne integrasjoner i Trilium med egendefinerte widgets, eller serversidelogikk.", + "import_export_description": "Samhandle med andre programmer ved hjelp av Markdown, ENEX og OML.", + "api_description": "Ved hjelp av den innebygde REST-APIen kan du programmatisk samhandle med Trilium." }, "collections": { "title": "Samlinger", @@ -66,7 +72,11 @@ "geomap_title": "Geokart", "presentation_title": "Presentasjon", "board_title": "Kanbantavle", - "geomap_description": "Planlegg ferien din eller merk deg dine interessepunkter på et geografisk kart ved hjelp av definerbare markører. Vis lagrede GPX-spor for å se reisen din." + "geomap_description": "Planlegg ferien din eller merk deg dine interessepunkter på et geografisk kart ved hjelp av definerbare markører. Vis lagrede GPX-spor for å se reisen din.", + "calendar_description": "Organiser dine personlige eller jobb-arrangement ved hjelp av kalender, med støtte for heldags- og flerdagsarrangement. Få rask oversikt over dine arrangementer med ukes- måneds- og årsvisning. Dra og slipp hendelser for enkelt å gjøre endringer.", + "table_description": "Vis og rediger informasjon om notater i tabellform, med ulike kolonnetyper som tekst, nummer, avkrysningsbokser, dato og tid, lenker, farger og støtte for relasjoner. Du kan også vise notater i et hierarkisk tre i tabellen.", + "board_description": "Organiser oppgaver eller prosjekter i en Kanbantavle hvor du enkelt kan lage nye elementer og kolonner, og endre status på elementer ved å dra de rundt på tavlen.", + "presentation_description": "Organiser informasjon i lysbilder og presenter dem i fullskjermmodus med myke overganger. Lysbildene kan også eksporteres til PDF for enkel deling." }, "header": { "documentation": "Dokumentasjon", @@ -84,14 +94,19 @@ "title": "Støtt oss", "financial_donations_title": "Finansiell donasjon", "github_sponsors": "GitHub Sponsors", - "financial_donations_description": "Trilium er bygget og vedlikeholdt med flere hundre timers arbeid. Ditt bidrag hjelper å holde det åpen kildekode, forbedre funksjonalitet og dekker driftskostnader." + "financial_donations_description": "Trilium er bygget og vedlikeholdt med flere hundre timers arbeid. Ditt bidrag hjelper å holde det åpen kildekode, forbedre funksjonalitet og dekker driftskostnader.", + "financial_donations_cta": "Vurder gjerne å støtte hovedutvikleren (eliandoran) av programmet via:", + "buy_me_a_coffee": "Buy Me A Coffee" }, "download_helper_desktop_windows": { "download_scoop": "Scoop", "title_x64": "Windows 64-bit", "download_zip": "Portable (.zip)", "title_arm64": "Windows på ARM", - "download_exe": "Last ned installasjonsprogram (.exe)" + "download_exe": "Last ned installasjonsprogram (.exe)", + "description_x64": "Kompatibel med Intel- eller AMD-enheter som kjører Windows 10 og 11.", + "description_arm64": "Kompatibel med ARM-enheter (for eksempel Qualcomm Snapdragon).", + "quick_start": "For å installere via Winget:" }, "download_helper_desktop_linux": { "download_deb": ".deb", @@ -101,21 +116,31 @@ "download_aur": "AUR", "title_x64": "Linux 64-bit", "download_zip": "Portable (.zip)", - "title_arm64": "Linux på ARM" + "title_arm64": "Linux på ARM", + "description_x64": "For de fleste Linux-distribusjoner, kompatibelt med x86_64-arkitektur.", + "description_arm64": "For ARM-baserte Linux-distribusjoner, kompatibelt med aarch64-arkitektur.", + "quick_start": "Velg egnet pakkeformat avhengig av din distribusjon:" }, "download_helper_server_docker": { "download_ghcr": "ghcr.io", "download_dockerhub": "Docker Hub", - "title": "Selv-hostet med Docker" + "title": "Selv-hostet med Docker", + "description": "Installer enkelt på Windows, Linux eller macOS ved bruk av en Docker-container." }, "download_helper_desktop_macos": { "download_homebrew_cask": "Homebrew Cask", "download_zip": "Portable (.zip)", "title_x64": "macOS for Intel", - "download_dmg": "Last ned installasjonsprogram (.dmg)" + "download_dmg": "Last ned installasjonsprogram (.dmg)", + "title_arm64": "macOS for Apple Silicon", + "description_x64": "For Intel-baserte Mac-er med macOS Monterey eller nyere.", + "description_arm64": "For Apple Silicon Mac-er som de med M1- og M2-chiper.", + "quick_start": "For å installere via Homebrew:" }, "final_cta": { - "get_started": "Kom i gang" + "get_started": "Kom i gang", + "title": "Klar for å begynne med Trilium Notes?", + "description": "Skap din personlige kunnskapsbase med kraftig funksjonalitet og fullt personvern." }, "components": { "link_learn_more": "Lær mer..." @@ -125,7 +150,8 @@ "platform_small": "for {{platform}}", "linux_small": "for Linux", "platform_big": "v{{version}} for {{platform}}", - "linux_big": "v{{version}} for Linux" + "linux_big": "v{{version}} for Linux", + "more_platforms": "Flere plattformer og serveroppsett" }, "footer": { "copyright_and_the": " og ", @@ -135,16 +161,40 @@ "download_tar_x64": "x64 (.tar.xz)", "download_tar_arm64": "ARM (.tar.xz)", "download_nixos": "NixOS modul", - "title": "Selv-hostet på Linux" + "title": "Selv-hostet på Linux", + "description": "Installer Trilium Notes på din egen server eller VPS, kompatibel med de fleste distribusjoner." }, "download_helper_server_hosted": { "title": "Betalt hosting", - "download_triliumcc": "Alternativt sjekk trilium.cc" + "download_triliumcc": "Alternativt sjekk trilium.cc", + "description": "Trilium Notes driftet på PikaPods, en betalt tjeneste for enkel tilgang og administrasjon. Ikke direkte tilknyttet Trilium-teamet.", + "download_pikapod": "Installer på PikaPods" }, "faq": { - "title": "Ofte stilte spørsmål" + "title": "Ofte stilte spørsmål", + "mobile_question": "Finnes det en mobil applikasjon?", + "mobile_answer": "Foreløpig er det ikke noe offisiell mobil applikasjon. Men hvis du har en serverinstans kan du koble til denne med en nettleser, og også installere den som en progressiv web-app. For Android finnes det en uoffisiell applikasjon med navn TriliumDroid som også fungerer offline (samme som en skrivebordsklient).", + "database_question": "Hvor lagres dataene?", + "database_answer": "Alle notater lagres i en SQLite-database i en programmappe. Årsaken til at Trilium bruker database i stedet for rene tekstfiler er både ytelse og at visse funksjoner ellers ville vært vanskelig å implementere, slik som klonede notater (samme notat flere steder). For å finne programmappen, åpne \"om\"-vinduet i programmet.", + "server_question": "Trenger jeg en server for å bruke Trilium?", + "server_answer": "Nei, serveren tillater tilgang via nettleser og håndterer synkronisering hvis du har flere enheter. For å komme i gang er det nok å laste ned skrivebordsprogrammet og begynne med det.", + "scaling_question": "Hvor godt skalerer programmet med store mengder notater?", + "scaling_answer": "Avhengig av bruk burde programmet kunne håndtere minst 100.000 notater uten problemer. Merk at synkroniseringen noen ganger kan feile ved opplasting av mange store filer (1GB per fil) siden Trilium er ment for å være en kunnskapsbase mer enn et fillager (som for eksempel NextCloud).", + "network_share_question": "Kan jeg dele databasen min over nettverksdeling?", + "network_share_answer": "Nei, det er stort sett ikke en god ide å dele en SQLite-database over nettverksdeling. Selv om det kan fungere, er det sjanser for at databasen kan bli ødelagt grunnet problemer med fillåsing over nettverk.", + "security_question": "Hvordan er mine data beskyttet?", + "security_answer": "Som standard blir ikke notater kryptert og kan leses direkte fra databasen. Når et notat er markert kryptert, blir det kryptert med AES-128-CBC." }, "404": { - "title": "404: Siden ble ikke funnet" + "title": "404: Siden ble ikke funnet", + "description": "Siden ble ikke funnet. Den kan ha blitt slettet eller adressen er feil." + }, + "contribute": { + "title": "Andre måter å bidra", + "way_translate": "Oversett programmet til ditt språk via Weblate.", + "way_community": "Ta del i felleskapet på GitHub Discussions eller på Matrix.", + "way_reports": "Meld feil via GitHub issues.", + "way_document": "Hjelp oss å forbedre dokumentasjonen ved å fortelle om mangler, eller bidra med veiledninger, Ofte Stilte Spørsmål eller tutorials.", + "way_market": "Spre ordet: Del Trilium Notes med venner, på blogger eller i sosiale media." } } From 73e94d385e54829c0f77510a1dcf5c6706079326 Mon Sep 17 00:00:00 2001 From: Yatrik Patel Date: Wed, 7 Jan 2026 00:42:19 +0100 Subject: [PATCH 60/67] Translated using Weblate (Hindi) Currently translated at 5.9% (23 of 389 strings) Translation: Trilium Notes/Server Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/ --- apps/server/src/assets/translations/hi/server.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/server/src/assets/translations/hi/server.json b/apps/server/src/assets/translations/hi/server.json index c994dc43d..6b5b8eba5 100644 --- a/apps/server/src/assets/translations/hi/server.json +++ b/apps/server/src/assets/translations/hi/server.json @@ -11,6 +11,17 @@ "move-note-up": "नोट को ऊपर ले जाएं", "move-note-down": "नोट को नीचे ले जाएं", "note-clipboard": "नोट क्लिपबोर्ड", - "duplicate-subtree": "डुप्लिकेट सबट्री" + "duplicate-subtree": "डुप्लिकेट सबट्री", + "open-new-tab": "नया टैब खोलें", + "second-tab": "लिस्ट में दूसरी टैब एक्टिवेट करें", + "third-tab": "लिस्ट में तीसरी टैब एक्टिवेट करें", + "fourth-tab": "लिस्ट में चौथी टैब एक्टिवेट करें", + "sixth-tab": "लिस्ट में छठी टैब एक्टिवेट करें", + "seventh-tab": "लिस्ट में सातवीं टैब एक्टिवेट करें", + "eight-tab": "लिस्ट में आठवीं टैब एक्टिवेट करें", + "ninth-tab": "लिस्ट में नौवीं टैब एक्टिवेट करें", + "last-tab": "लिस्ट में आखिरी टैब एक्टिवेट करें", + "show-sql-console": "\"SQL कंसोल\" पेज खोलें", + "show-backend-log": "\"बैकेंड लॉग\" पेज खोलें" } } From 869db5e47846c9f70b9ae6fce1d832d2766fdde8 Mon Sep 17 00:00:00 2001 From: Yatrik Patel Date: Wed, 7 Jan 2026 00:49:46 +0100 Subject: [PATCH 61/67] Translated using Weblate (Hindi) Currently translated at 0.9% (17 of 1751 strings) Translation: Trilium Notes/Client Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/ --- apps/client/src/translations/hi/translation.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/client/src/translations/hi/translation.json b/apps/client/src/translations/hi/translation.json index 6d3e6dfde..81c56863a 100644 --- a/apps/client/src/translations/hi/translation.json +++ b/apps/client/src/translations/hi/translation.json @@ -31,5 +31,17 @@ }, "add_link": { "note": "नोट" + }, + "bulk_actions": { + "other": "अन्य" + }, + "clone_to": { + "search_for_note_by_its_name": "नोट क नाम से नोट खोजें" + }, + "confirm": { + "also_delete_note": "नोट भी डिलीट करें" + }, + "delete_notes": { + "delete_notes_preview": "नोट्स प्रिव्यू डिलीट करें" } } From a627d1f96ef4f5525ff0b89bc8a36c11ed7b5937 Mon Sep 17 00:00:00 2001 From: pythaac Date: Tue, 6 Jan 2026 18:32:07 +0100 Subject: [PATCH 62/67] Translated using Weblate (Korean) Currently translated at 76.3% (116 of 152 strings) Translation: Trilium Notes/Website Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/ --- .../src/translations/ko/translation.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/website/src/translations/ko/translation.json b/apps/website/src/translations/ko/translation.json index d8d5c861d..b65d46302 100644 --- a/apps/website/src/translations/ko/translation.json +++ b/apps/website/src/translations/ko/translation.json @@ -130,6 +130,23 @@ "mobile_question": "모바일 앱이 있나요?", "mobile_answer": "현재 공식적인 모바일 앱은 없습니다. 하지만, 서버 인스턴스를 가지고 있다면 웹 브라우저를 이용해 접근하거나 PWA로 설치할 수 있습니다. 안드로이드에는 (데스크탑 클라이언트처럼)오프라인에서도 작동하는 TriliumDroid라는 비공식 앱이 있습니다.", "database_question": "어디에 데이터가 저장되나요?", - "server_question": "Trilium을 사용하기 위해 서버가 필요한가요?" + "server_question": "Trilium을 사용하기 위해 서버가 필요한가요?", + "title": "자주 묻는 질문", + "database_answer": "모든 노트는 애플리케이션 폴더의 SQLite 데이터베이스에 저장됩니다. Trilium이 텍스트 파일 대신 데이터베이스를 사용하는 이유는 성능과 기능 모두 구현하기 훨씬 어렵기 때문입니다(트리 여러 위치에 같은 노트를 두는 Clone과 같은 기능). 애플리케이션 폴더를 찾으려면 About 창으로 가세요.", + "server_answer": "아니요, 서버는 웹 브라우저를 통해 접속할 수 있도록 허용하며, 여러 기기를 사용하는 경우 동기화를 관리합니다. 시작하려면 데스크톱 애플리케이션을 다운로드하여 사용하기만 하면 됩니다.", + "scaling_question": "이 애플리케이션은 얼마나 많은 노트를 처리할 수 있나요?", + "scaling_answer": "사용량에 따라 다르겠지만, 이 애플리케이션은 최소 10만 개의 노트를 문제없이 처리할 수 있습니다. 다만, Trilium은 (NextCloud와 같은) 파일 저장소라기보다는 지식 기반 애플리케이션에 가깝기 때문에, 대용량 파일(파일당 1GB 이상)을 많이 업로드할 경우 동기화 과정이 실패할 수 있다는 점에 유의하십시오.", + "network_share_question": "내 데이터베이스를 네트워크 드라이브로 공유할 수 있나요?", + "network_share_answer": "아니요, 일반적으로 SQLite 데이터베이스를 네트워크 드라이브로 공유하는 것은 좋지 않습니다. 경우에 따라 작동할 수도 있지만, 네트워크를 통한 파일 잠금이 완벽하지 않아 데이터베이스가 손상될 가능성이 있습니다.", + "security_question": "내 데이터는 어떻게 보호되나요?", + "security_answer": "기본적으로 노트는 암호화되지 않으며 데이터베이스에서 직접 읽을 수 있습니다. 노트를 암호화 대상으로 표시하면, AES-128-CBC를 사용하여 암호화됩니다." + }, + "final_cta": { + "title": "Trilium Notes를 시작할 준비가 되셨나요?", + "description": "강력한 기능과 완벽한 개인 정보 보호를 통해 나만의 지식 기반을 구축하세요.", + "get_started": "시작하기" + }, + "components": { + "link_learn_more": "자세히 알아보기..." } } From d747c94450717430b3cd531b16c1bc0aab3bd78f Mon Sep 17 00:00:00 2001 From: Yatrik Patel Date: Wed, 7 Jan 2026 00:39:54 +0100 Subject: [PATCH 63/67] Translated using Weblate (Hindi) Currently translated at 3.4% (4 of 116 strings) Translation: Trilium Notes/README Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/ --- docs/README-hi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README-hi.md b/docs/README-hi.md index 173b83759..c47ebdb11 100644 --- a/docs/README-hi.md +++ b/docs/README-hi.md @@ -107,7 +107,7 @@ Our documentation is available in multiple formats: maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and [note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map) for visualizing notes and their relations -* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/) +* [Mind Elixir](https://docs.mind-elixir.com/) पर आधारित माइंड मैप्स * [Geo maps](https://docs.triliumnotes.org/user-guide/collections/geomap) with location pins and GPX tracks * [Scripting](https://docs.triliumnotes.org/user-guide/scripts) - see [Advanced @@ -157,7 +157,7 @@ compatible with the latest zadam/trilium version of versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration. -## 💬 Discuss with us +## 💬 हमारे साथ चर्चा करें Feel free to join our official conversations. We would love to hear what features, suggestions, or issues you may have! From 3d8cbc81c40c4b9511467def7f41c48345e06164 Mon Sep 17 00:00:00 2001 From: Argann Bonneau Date: Wed, 7 Jan 2026 12:01:36 +0100 Subject: [PATCH 64/67] Translated using Weblate (French) Currently translated at 94.5% (1656 of 1751 strings) Translation: Trilium Notes/Client Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/ --- apps/client/src/translations/fr/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json index e8fa4dcc3..2900c5de5 100644 --- a/apps/client/src/translations/fr/translation.json +++ b/apps/client/src/translations/fr/translation.json @@ -21,7 +21,7 @@ }, "bundle-error": { "title": "Echec du chargement d'un script personnalisé", - "message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}" + "message": "Le script n'a pas pu être exécuté à cause de\n\n{{message}}" }, "widget-list-error": { "title": "Impossible d'obtenir la liste des widgets depuis le serveur" From 2c92ae8898a54083bb3c7f5fd07e730ce66d68a1 Mon Sep 17 00:00:00 2001 From: Giovi Date: Wed, 7 Jan 2026 10:08:34 +0100 Subject: [PATCH 65/67] Translated using Weblate (Italian) Currently translated at 100.0% (1751 of 1751 strings) Translation: Trilium Notes/Client Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/ --- .../src/translations/it/translation.json | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/client/src/translations/it/translation.json b/apps/client/src/translations/it/translation.json index 47c44269d..3528cce11 100644 --- a/apps/client/src/translations/it/translation.json +++ b/apps/client/src/translations/it/translation.json @@ -1895,7 +1895,11 @@ "create-child-note": "Crea nota figlio", "unhoist": "Sganciare", "toggle-sidebar": "Attiva/disattiva la barra laterale", - "dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione." + "dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione.", + "clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}", + "clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})", + "shared-indicator-tooltip": "Questa nota è condivisa pubblicamente", + "shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}" }, "title_bar_buttons": { "window-on-top": "Mantieni la finestra in primo piano" @@ -2200,7 +2204,14 @@ "execute_sql_description": "Questa nota è una nota SQL. Clicca per eseguire la query SQL.", "shared_copy_to_clipboard": "Copia link negli appunti", "shared_open_in_browser": "Apri il link nel browser", - "shared_unshare": "Rimuovi condivisione" + "shared_unshare": "Rimuovi condivisione", + "save_status_saved": "Salvato", + "save_status_saving": "Salvataggio in corso...", + "save_status_unsaved": "Non salvato", + "save_status_error": "Salvataggio non riuscito", + "save_status_saving_tooltip": "Le modifiche sono state salvate.", + "save_status_unsaved_tooltip": "Ci sono modifiche non salvate. Verranno salvate automaticamente tra un attimo.", + "save_status_error_tooltip": "Si è verificato un errore durante il salvataggio della nota. Se possibile, prova a copiare il contenuto della nota altrove e a ricaricare l'applicazione." }, "breadcrumb": { "workspace_badge": "Area di lavoro", @@ -2243,5 +2254,18 @@ "empty_button": "Nascondi il pannello", "toggle": "Attiva/disattiva pannello destro", "custom_widget_go_to_source": "Vai al codice sorgente" + }, + "pdf": { + "attachments_one": "{{count}} allegato", + "attachments_many": "{{count}} allegati", + "attachments_other": "{{count}} allegati", + "layers_one": "{{count}} livello", + "layers_many": "{{count}} livelli", + "layers_other": "{{count}} livelli", + "pages_one": "{{count}} pagina", + "pages_many": "{{count}} pagine", + "pages_other": "{{count}} pagine", + "pages_alt": "Pagina {{pageNumber}}", + "pages_loading": "Caricamento in corso..." } } From d807984be4bb381084c819c627ebff60a1d1f59e Mon Sep 17 00:00:00 2001 From: Rafa Osuna Date: Wed, 7 Jan 2026 14:20:52 +0100 Subject: [PATCH 66/67] Translated using Weblate (Spanish) Currently translated at 92.7% (1624 of 1751 strings) Translation: Trilium Notes/Client Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/ --- apps/client/src/translations/es/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json index e13f37fcf..252cce17f 100644 --- a/apps/client/src/translations/es/translation.json +++ b/apps/client/src/translations/es/translation.json @@ -162,7 +162,8 @@ "other": "Otro", "quickSearch": "centrarse en la entrada de búsqueda rápida", "inPageSearch": "búsqueda en la página", - "title": "Hoja de ayuda" + "title": "Hoja de ayuda", + "editShortcuts": "Editar atajos de teclado" }, "import": { "importIntoNote": "Importar a nota", From 3800fb85ebb17498a834e5efdf3111af9ef6247a Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 7 Jan 2026 16:09:09 +0100 Subject: [PATCH 67/67] Translated using Weblate (German) Currently translated at 95.4% (1672 of 1751 strings) Translation: Trilium Notes/Client Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/ --- apps/client/src/translations/de/translation.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json index c0ccea2ea..c9bc04b7f 100644 --- a/apps/client/src/translations/de/translation.json +++ b/apps/client/src/translations/de/translation.json @@ -25,7 +25,8 @@ }, "widget-list-error": { "title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen" - } + }, + "open-script-note": "Script-Notiz öffnen" }, "add_link": { "add_link": "Link hinzufügen", @@ -208,7 +209,8 @@ "info": { "modalTitle": "Infonachricht", "closeButton": "Schließen", - "okButton": "OK" + "okButton": "OK", + "copy_to_clipboard": "In die Zwischenablage kopieren" }, "jump_to_note": { "search_button": "Suche im Volltext", @@ -695,7 +697,9 @@ "export_as_image": "Als Bild exportieren", "export_as_image_png": "PNG (Raster)", "export_as_image_svg": "SVG (Vektor)", - "note_map": "Notizen Karte" + "note_map": "Notizen Karte", + "view_revisions": "Notizrevisionen", + "advanced": "Erweitert" }, "onclick_button": { "no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"