From c5d282d203cdaad8ab7db1c882aefdd6256175d2 Mon Sep 17 00:00:00 2001 From: meinzzzz Date: Thu, 20 Nov 2025 00:09:10 +0100 Subject: [PATCH 001/196] 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 5a18b797d3..583a448cca 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 6d6982ae23..b3475309f5 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 4c4a2794c5..2ac4c79cd4 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 2d1c597938..ef818e203a 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 0000000000..a09ec641d6 --- /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 0000000000..871f23e72b --- /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 5a392c0db0..a55fda523b 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 0000000000..6f755ce79e --- /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 0000000000..d62f08d2ed --- /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 3b7b4047f9..45446d8f77 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 ad305250f5..c23634f4cb 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 002/196] 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 a09ec641d6..340314dc3f 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 003/196] 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 34f845f916..0f3e8a37aa 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 2ac4c79cd4..76290626fe 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 ef818e203a..7c57fce8f4 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 340314dc3f..148ae7f141 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 aacb56ee45..903192f4f3 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 004/196] 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 ba0fbb97b9..d8db8801dc 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 8d3b0c5834..12eac25ced 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 b3475309f5..eac4d7b223 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 76290626fe..504adf77aa 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 148ae7f141..689ec891aa 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 903192f4f3..1e470a4896 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 005/196] 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 d8db8801dc..ba0fbb97b9 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 12eac25ced..8d3b0c5834 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 006/196] 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 007/196] 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 7c57fce8f4..f5f630dffc 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 45446d8f77..5821c7fd29 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 008/196] 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 b2c4e3c464..9321d866fe 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 009/196] 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 5821c7fd29..6a15c852a0 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 010/196] 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 6a15c852a0..9537cdbb0d 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 011/196] 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 f5f630dffc..3a1d913d8b 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 689ec891aa..9dbce2903c 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 871f23e72b..4c994778d4 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 012/196] 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 3a1d913d8b..46f6e1aaf6 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 013/196] 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 9537cdbb0d..300e759d3c 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 014/196] 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 46f6e1aaf6..ac25bc5f0e 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 1e470a4896..65308fda25 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 015/196] 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 0f3e8a37aa..34f845f916 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 ac25bc5f0e..ab904c86ef 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 65308fda25..54486943a0 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 016/196] 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 ab904c86ef..cb665347c4 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 9dbce2903c..d337a9bdb6 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 4c994778d4..5831fad08f 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 300e759d3c..3f2546c247 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 017/196] 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 cb665347c4..f44b020ee4 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 018/196] remove gitignore unneccesary changes --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9321d866fe..9ea55440e1 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 019/196] 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 6f755ce79e..0000000000 --- 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 d62f08d2ed..0000000000 --- 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 020/196] 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 d337a9bdb6..6b49308050 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 021/196] 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 504adf77aa..a27ee87fd5 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 f44b020ee4..15e3ed55d0 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 6b49308050..b5f36e94e4 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 fab16262e9..254c306abb 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 5831fad08f..81593ec76c 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 022/196] 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 3f2546c247..270e86568a 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 023/196] 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 254c306abb..42a4c0df17 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 024/196] 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 270e86568a..c2a1476bb8 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 025/196] 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 b5f36e94e4..a3863077cd 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 81593ec76c..cc468b4343 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 026/196] 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 a3863077cd..f761be787d 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 027/196] 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 15e3ed55d0..d7ceb2f20f 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 028/196] 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 c2a1476bb8..1465b9e1b7 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 029/196] 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 f761be787d..a2d540228e 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 42a4c0df17..87af15d08d 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 1265078502..5739843684 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 030/196] 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 a27ee87fd5..5f36396d14 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 d7ceb2f20f..5e9d069cf7 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 0000000000..72c4e08fe1 --- /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 a2d540228e..0000000000 --- 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 cc468b4343..0000000000 --- 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 4493420e1d..b379ad8c68 100644 --- a/packages/ckeditor5-math/tests/index.ts +++ b/packages/ckeditor5-math/tests/index.ts @@ -3,6 +3,20 @@ import Math from '../src/math'; import AutoformatMath from '../src/autoformatmath'; import { describe, it, expect } from 'vitest'; +// Suppress MathLive errors during async cleanup in tests +if (typeof window !== 'undefined') { + window.addEventListener('unhandledrejection', event => { + if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) { + event.preventDefault(); + } + }); + window.addEventListener('error', event => { + if (event.message?.includes('options') || event.message?.includes('mathlive')) { + event.preventDefault(); + } + }); +} + describe( 'CKEditor5 Math DLL', () => { it( 'exports Math', () => { expect( MathDll ).to.equal( Math ); diff --git a/packages/ckeditor5-math/tests/lazyload.ts b/packages/ckeditor5-math/tests/lazyload.ts index 5739843684..173fae184d 100644 --- a/packages/ckeditor5-math/tests/lazyload.ts +++ b/packages/ckeditor5-math/tests/lazyload.ts @@ -2,6 +2,20 @@ import { ClassicEditor, type EditorConfig } from 'ckeditor5'; import MathUI from '../src/mathui'; import { describe, beforeEach, it, afterEach, expect } from "vitest"; +// Suppress MathLive errors during async cleanup +if (typeof window !== 'undefined') { + window.addEventListener('unhandledrejection', event => { + if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) { + event.preventDefault(); + } + }); + window.addEventListener('error', event => { + if (event.message?.includes('options') || event.message?.includes('mathlive')) { + event.preventDefault(); + } + }); +} + describe( 'Lazy load', () => { let editorElement: HTMLDivElement; let editor: ClassicEditor; @@ -24,11 +38,14 @@ describe( 'Lazy load', () => { beforeEach( () => { editorElement = document.createElement( 'div' ); document.body.appendChild( editorElement ); - lazyLoadInvoked = false; } ); - afterEach( () => { + afterEach( async () => { + if ( mathUIFeature?.formView ) { + mathUIFeature._hideUI(); + } + await new Promise( resolve => setTimeout( resolve, 50 ) ); editorElement.remove(); return editor.destroy(); } ); diff --git a/packages/ckeditor5-math/tests/mathui.ts b/packages/ckeditor5-math/tests/mathui.ts index 6317b5e66c..94becd000e 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 1465b9e1b7..0d401fa5e8 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 fb7ccfff03..47a35ced82 100644 --- a/packages/ckeditor5-math/vitest.config.ts +++ b/packages/ckeditor5-math/vitest.config.ts @@ -22,6 +22,9 @@ export default defineConfig( { include: [ 'tests/**/*.[jt]s' ], + exclude: [ + 'tests/setup.ts' + ], globals: true, watch: false, coverage: { From 8a385972fc36cb6f9d1e2f292ff68f0589655657 Mon Sep 17 00:00:00 2001 From: Meinzzzz Date: Mon, 8 Dec 2025 18:49:06 +0100 Subject: [PATCH 031/196] 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 5f36396d14..4bba3f304f 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 5e9d069cf7..f8d8901cca 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 72c4e08fe1..c0e78b2748 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 032/196] 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 0d401fa5e8..628105078e 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 033/196] 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 c0e78b2748..31171d0129 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 034/196] 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 f8d8901cca..cb96826a09 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 31171d0129..7df5c590df 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 035/196] 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 cb96826a09..2c327517ce 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 7df5c590df..b296c86aae 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 036/196] 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 4bba3f304f..8b41510e0a 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 037/196] 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 8b41510e0a..030e34e4dc 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 2c327517ce..8e7d62ed4f 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 b296c86aae..a5531fca8f 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 628105078e..585b0ba702 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 038/196] 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 8e7d62ed4f..1c2403e18f 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 a5531fca8f..945bebc794 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 039/196] 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 1c2403e18f..d459354fb1 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 945bebc794..ace20848cc 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 87af15d08d..aa1027329e 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 040/196] 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 ace20848cc..f14a5c9a8c 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 041/196] 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 f14a5c9a8c..a1285f9914 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 585b0ba702..fff2feef4b 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 042/196] 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 030e34e4dc..3e3da2744d 100644 --- a/packages/ckeditor5-math/src/mathui.ts +++ b/packages/ckeditor5-math/src/mathui.ts @@ -55,9 +55,9 @@ export default class MathUI extends Plugin { this._balloon.showStack( 'main' ); - requestAnimationFrame(() => { + 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 a1285f9914..65bf31bba9 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 043/196] 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 d459354fb1..487a96de0f 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 044/196] 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 487a96de0f..52d3b05120 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 fff2feef4b..a5d55f2f1b 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 045/196] 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 65bf31bba9..a3b93f3e17 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 046/196] 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 a3b93f3e17..027b7aab9b 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 047/196] 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 7ff8c4155b..711ebbee52 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "jiti": "2.6.1", "jsonc-eslint-parser": "2.4.2", "react-refresh": "0.18.0", - "rollup-plugin-webpack-stats": "2.1.8", + "rollup-plugin-webpack-stats": "2.1.9", "tslib": "2.8.1", "tsx": "4.21.0", "typescript": "~5.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31fc4bd627..d2220e08d3 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 048/196] 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 e4fde650b9..f5c6b0088a 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 049/196] 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 f5c6b0088a..7aaa5739d3 100644 --- a/apps/client/src/widgets/sql_result.tsx +++ b/apps/client/src/widgets/sql_result.tsx @@ -23,7 +23,7 @@ export default function SqlResults() { {t("sql_result.no_rows")} ) : ( -
+
{results?.map(rows => { // inserts, updates if (typeof rows === "object" && !Array.isArray(rows)) { From 5b95b9875b6b7273aaeec0f263b9022f5fa54631 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 5 Jan 2026 19:27:44 +0800 Subject: [PATCH 050/196] feat(tree): open notes in new window from tree --- apps/client/src/components/app_context.ts | 1 + apps/client/src/menus/tree_context_menu.ts | 6 ++++++ apps/client/src/translations/en/translation.json | 1 + 3 files changed, 8 insertions(+) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 560a004382..b5f203b245 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -154,6 +154,7 @@ export type CommandMappings = { }; openInTab: ContextMenuCommandData; openNoteInSplit: ContextMenuCommandData; + openNoteInWindow: ContextMenuCommandData; openNoteInPopup: ContextMenuCommandData; toggleNoteHoisting: ContextMenuCommandData; insertNoteAfter: ContextMenuCommandData; diff --git a/apps/client/src/menus/tree_context_menu.ts b/apps/client/src/menus/tree_context_menu.ts index 51f9912b32..81d4878b10 100644 --- a/apps/client/src/menus/tree_context_menu.ts +++ b/apps/client/src/menus/tree_context_menu.ts @@ -79,6 +79,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener | null)[] = [ { title: t("tree-context-menu.open-in-a-new-tab"), command: "openInTab", shortcut: "Ctrl+Click", uiIcon: "bx bx-link-external", enabled: noSelectedNotes }, { title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes }, + { title: t("tree-context-menu.open-in-a-new-window"), command: "openNoteInWindow", uiIcon: "bx bx-window-open", enabled: noSelectedNotes }, { title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes }, isHoisted @@ -309,6 +310,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener Date: Mon, 5 Jan 2026 21:03:32 +0200 Subject: [PATCH 051/196] 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 3c852ba140..6a021a0b7e 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 052/196] 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 a409fa437a..dd4e616d3d 100644 --- a/packages/ckeditor5/src/i18n.ts +++ b/packages/ckeditor5/src/i18n.ts @@ -50,6 +50,11 @@ const LOCALE_MAPPINGS: Record = { coreTranslation: () => import("ckeditor5/translations/ja.js"), premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ja.js"), }, + pl: { + languageCode: "pl", + coreTranslation: () => import("ckeditor5/translations/pl.js"), + premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pl.js"), + }, pt: { languageCode: "pt", coreTranslation: () => import("ckeditor5/translations/pt.js"), From 47fd2affa4e3e4684efc65f871f7047d19cd9266 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 6 Jan 2026 00:59:32 +0200 Subject: [PATCH 053/196] feat(tree): use direct DOM manipulation instead of jQuery --- apps/client/src/widgets/note_tree.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index 9a2c61d227..742d6490a5 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -153,7 +153,7 @@ const TPL = /*html*/` const MAX_SEARCH_RESULTS_IN_TREE = 100; // this has to be hanged on the actual elements to effectively intercept and stop click event -const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation(); +const cancelClickPropagation: (e: JQuery.ClickEvent | MouseEvent) => void = (e) => e.stopPropagation(); // TODO: Fix once we remove Node.js API from public type Timeout = NodeJS.Timeout | string | number | undefined; @@ -652,12 +652,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { && !note.isLaunchBarConfig() && !note.noteId.startsWith("_help") ) { - const $createChildNoteButton = $(``).on( - "click", - cancelClickPropagation - ); - - $span.append($createChildNoteButton); + const createChildEl = document.createElement("span"); + createChildEl.className = "tree-item-button tn-icon add-note-button bx bx-plus"; + createChildEl.title = t("note_tree.create-child-note"); + createChildEl.addEventListener("click", cancelClickPropagation); + node.span.append(createChildEl); } if (isHoistedNote) { From bde6068f2d4da3b63c1a40ee7582da1a83e77436 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 6 Jan 2026 01:07:37 +0200 Subject: [PATCH 054/196] refactor(tree): extract enchance title into separate method --- apps/client/src/widgets/note_tree.ts | 192 ++++++++++++++------------- 1 file changed, 97 insertions(+), 95 deletions(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index 742d6490a5..3e9481458c 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -598,101 +598,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { clones: { highlightActiveClones: true }, - async enhanceTitle ( - event: Event, - data: { - node: Fancytree.FancytreeNode; - noteId: string; - } - ) { - const node = data.node; - - if (!node.data.noteId) { - // if there's "non-note" node, then don't enhance - // this can happen for e.g. "Load error!" node - return; - } - - const note = await froca.getNote(node.data.noteId, true); - - if (!note) { - return; - } - - const activeNoteContext = appContext.tabManager.getActiveContext(); - - const $span = $(node.span); - - $span.find(".tree-item-button").remove(); - $span.find(".note-indicator-icon").remove(); - - const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root"; - - if (note.hasLabel("workspace") && !isHoistedNote) { - const $enterWorkspaceButton = $(``).on( - "click", - cancelClickPropagation - ); - - $span.append($enterWorkspaceButton); - } - - if (note.type === "search") { - const $refreshSearchButton = $(``).on( - "click", - cancelClickPropagation - ); - - $span.append($refreshSearchButton); - } - - // TODO: Deduplicate with server's notes.ts#getAndValidateParent - if (!["search", "launcher"].includes(note.type) - && !note.isOptions() - && !note.isLaunchBarConfig() - && !note.noteId.startsWith("_help") - ) { - const createChildEl = document.createElement("span"); - createChildEl.className = "tree-item-button tn-icon add-note-button bx bx-plus"; - createChildEl.title = t("note_tree.create-child-note"); - createChildEl.addEventListener("click", cancelClickPropagation); - node.span.append(createChildEl); - } - - if (isHoistedNote) { - const $unhoistButton = $(``).on("click", cancelClickPropagation); - - $span.append($unhoistButton); - } - - // Add clone indicator with tooltip if note has multiple parents - const parentNotes = note.getParentNotes(); - const realParents = parentNotes.filter( - (parent) => !["_share", "_lbBookmarks"].includes(parent.noteId) && parent.type !== "search" - ); - - if (realParents.length > 1) { - const parentTitles = realParents.map((p) => p.title).join(", "); - const tooltipText = realParents.length === 2 - ? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title }) - : t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles }); - - const $cloneIndicator = $(``); - $cloneIndicator.attr("title", tooltipText); - $span.find(".fancytree-title").append($cloneIndicator); - } - - // Add shared indicator with tooltip if note is shared - if (note.isShared()) { - const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId; - const shareUrl = `${location.origin}${location.pathname}share/${shareId}`; - const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl }); - - const $sharedIndicator = $(``); - $sharedIndicator.attr("title", tooltipText); - $span.find(".fancytree-title").append($sharedIndicator); - } - }, + enhanceTitle: buildEnhanceTitle(), // this is done to automatically lazy load all expanded notes after tree load loadChildren: (event, data) => { data.node.visit((subNode) => { @@ -1881,3 +1787,99 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { return items; } } + +function buildEnhanceTitle() { + return async (event: Event, + data: { + node: Fancytree.FancytreeNode; + noteId: string; + }) => { + const node = data.node; + + if (!node.data.noteId) { + // if there's "non-note" node, then don't enhance + // this can happen for e.g. "Load error!" node + return; + } + + const note = await froca.getNote(node.data.noteId, true); + + if (!note) { + return; + } + + const activeNoteContext = appContext.tabManager.getActiveContext(); + + const $span = $(node.span); + + $span.find(".tree-item-button").remove(); + $span.find(".note-indicator-icon").remove(); + + const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root"; + + if (note.hasLabel("workspace") && !isHoistedNote) { + const $enterWorkspaceButton = $(``).on( + "click", + cancelClickPropagation + ); + + $span.append($enterWorkspaceButton); + } + + if (note.type === "search") { + const $refreshSearchButton = $(``).on( + "click", + cancelClickPropagation + ); + + $span.append($refreshSearchButton); + } + + // TODO: Deduplicate with server's notes.ts#getAndValidateParent + if (!["search", "launcher"].includes(note.type) + && !note.isOptions() + && !note.isLaunchBarConfig() + && !note.noteId.startsWith("_help") + ) { + const createChildEl = document.createElement("span"); + createChildEl.className = "tree-item-button tn-icon add-note-button bx bx-plus"; + createChildEl.title = t("note_tree.create-child-note"); + createChildEl.addEventListener("click", cancelClickPropagation); + node.span.append(createChildEl); + } + + if (isHoistedNote) { + const $unhoistButton = $(``).on("click", cancelClickPropagation); + + $span.append($unhoistButton); + } + + // Add clone indicator with tooltip if note has multiple parents + const parentNotes = note.getParentNotes(); + const realParents = parentNotes.filter( + (parent) => !["_share", "_lbBookmarks"].includes(parent.noteId) && parent.type !== "search" + ); + + if (realParents.length > 1) { + const parentTitles = realParents.map((p) => p.title).join(", "); + const tooltipText = realParents.length === 2 + ? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title }) + : t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles }); + + const $cloneIndicator = $(``); + $cloneIndicator.attr("title", tooltipText); + $span.find(".fancytree-title").append($cloneIndicator); + } + + // Add shared indicator with tooltip if note is shared + if (note.isShared()) { + const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId; + const shareUrl = `${location.origin}${location.pathname}share/${shareId}`; + const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl }); + + const $sharedIndicator = $(``); + $sharedIndicator.attr("title", tooltipText); + $span.find(".fancytree-title").append($sharedIndicator); + } + }; +} From 0867b81c7ad492f6fe475a349f1853a52436e9aa Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 6 Jan 2026 01:13:31 +0200 Subject: [PATCH 055/196] feat(tree): use template for create child to improve performance --- apps/client/src/widgets/note_tree.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index 3e9481458c..b0446fae93 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -1789,11 +1789,16 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } function buildEnhanceTitle() { - return async (event: Event, + const createChildTemplate = document.createElement("span"); + createChildTemplate.className = "tree-item-button tn-icon add-note-button bx bx-plus"; + createChildTemplate.title = t("note_tree.create-child-note"); + createChildTemplate.addEventListener("click", cancelClickPropagation); + + return async function enhanceTitle(event: Event, data: { node: Fancytree.FancytreeNode; noteId: string; - }) => { + }) { const node = data.node; if (!node.data.noteId) { @@ -1841,11 +1846,7 @@ function buildEnhanceTitle() { && !note.isLaunchBarConfig() && !note.noteId.startsWith("_help") ) { - const createChildEl = document.createElement("span"); - createChildEl.className = "tree-item-button tn-icon add-note-button bx bx-plus"; - createChildEl.title = t("note_tree.create-child-note"); - createChildEl.addEventListener("click", cancelClickPropagation); - node.span.append(createChildEl); + node.span.append(createChildTemplate.cloneNode()); } if (isHoistedNote) { From d0cdcfc32c9cf03fec0e55f44dbfa73d97d3cf5e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 6 Jan 2026 01:21:43 +0200 Subject: [PATCH 056/196] refactor(tree): use loop for mini optimisation --- apps/client/src/widgets/note_tree.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index b0446fae93..c4d4ce0c22 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -1857,9 +1857,12 @@ function buildEnhanceTitle() { // Add clone indicator with tooltip if note has multiple parents const parentNotes = note.getParentNotes(); - const realParents = parentNotes.filter( - (parent) => !["_share", "_lbBookmarks"].includes(parent.noteId) && parent.type !== "search" - ); + const realParents: FNote[] = []; + for (const parent of parentNotes) { + if (parent.noteId !== "_share" && parent.noteId !== "_lbBookmarks" && parent.type !== "search") { + realParents.push(parent); + } + } if (realParents.length > 1) { const parentTitles = realParents.map((p) => p.title).join(", "); From dec4dafba61c084adbfcba012b2c38fede122049 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 6 Jan 2026 01:26:56 +0200 Subject: [PATCH 057/196] feat(tree): avoid async --- apps/client/src/widgets/note_tree.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index c4d4ce0c22..f815edfc9e 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -1807,11 +1807,8 @@ function buildEnhanceTitle() { return; } - const note = await froca.getNote(node.data.noteId, true); - - if (!note) { - return; - } + const note = froca.getNoteFromCache(node.data.noteId); + if (!note) return; const activeNoteContext = appContext.tabManager.getActiveContext(); From aff4f7e01091281895ab7bdaa2f60ec8f907b5b0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 6 Jan 2026 01:34:02 +0200 Subject: [PATCH 058/196] feat(tree): disable animation for performance --- apps/client/src/widgets/note_tree.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index f815edfc9e..8f4ead199d 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -353,6 +353,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { this.$tree.fancytree({ titlesTabbable: true, keyboard: true, + toggleEffect: false, extensions: ["dnd5", "clones", "filter"], source: treeData, scrollOfs: { From 639b1f2863a5489ef2f90943e29fdd49de99ec38 Mon Sep 17 00:00:00 2001 From: Yatrik Patel Date: Mon, 5 Jan 2026 04:00:48 +0100 Subject: [PATCH 059/196] 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 f268e30ad7..4146c0ca0e 100644 --- a/apps/website/src/translations/hi/translation.json +++ b/apps/website/src/translations/hi/translation.json @@ -13,6 +13,7 @@ "note_structure_description": "नोटों को पदानुक्रमिक रूप से व्यवस्थित किया जा सकता है। फ़ोल्डर्स की कोई आवश्यकता नहीं है, क्योंकि प्रत्येक नोट में उप-नोट हो सकते हैं। एक एकल नोट को पदानुक्रम में कई स्थानों पर जोड़ा जा सकता है।" }, "productivity_benefits": { - "protected_notes_title": "संरक्षित नोट्स" + "protected_notes_title": "संरक्षित नोट्स", + "web_clipper_title": "वेब क्लिपर" } } 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 060/196] 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 042fbb281b..40e39bec35 100644 --- a/apps/client/src/translations/hu/translation.json +++ b/apps/client/src/translations/hu/translation.json @@ -21,7 +21,13 @@ }, "bundle-error": { "title": "Nem sikerült betölteni az egyéni szkriptet", - "message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}" + "message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}" + }, + "widget-list-error": { + "title": "A Widget-ek letöltése sikertelen volt" + }, + "widget-render-error": { + "title": "Nem sikerült renderelni a React widget-et" } }, "add_link": { From b3c0be7559ede7cfee993af0ceb003492334e047 Mon Sep 17 00:00:00 2001 From: Yatrik Patel Date: Mon, 5 Jan 2026 04:03:15 +0100 Subject: [PATCH 061/196] 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 9b03fa2c37..c994dc43d1 100644 --- a/apps/server/src/assets/translations/hi/server.json +++ b/apps/server/src/assets/translations/hi/server.json @@ -10,6 +10,7 @@ "creating-and-moving-notes": "नोट्स बनाना और स्थानांतरित करना", "move-note-up": "नोट को ऊपर ले जाएं", "move-note-down": "नोट को नीचे ले जाएं", - "note-clipboard": "नोट क्लिपबोर्ड" + "note-clipboard": "नोट क्लिपबोर्ड", + "duplicate-subtree": "डुप्लिकेट सबट्री" } } 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 062/196] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=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 f9e3b5a586..9ad41c5527 100644 --- a/apps/website/src/translations/nb-NO/translation.json +++ b/apps/website/src/translations/nb-NO/translation.json @@ -10,13 +10,18 @@ "title": "Organiser tankene dine. Bygg din personlige kunnskapsbase.", "github": "GitHub", "get_started": "Kom i gang", - "dockerhub": "Docker Hub" + "dockerhub": "Docker Hub", + "screenshot_alt": "Screenshot fra Trilium Notes skrivebordsprogram", + "subtitle": "Trilium er en open-source-løsning for å ta notater og organisere en personlig kunnskapsbase. Kan brukes lokalt på arbeidsstasjonen din eller synkroniseres med en selv-hostet løsning for å ha dine notater med deg overalt." }, "organization_benefits": { "title": "Organisering", "note_structure_title": "Notatstruktur", "hoisting_title": "Arbeidsflate og fokusering", - "attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler." + "attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler.", + "note_structure_description": "Notater kan arrangeres herarkisk. Det trengs ikke mapper, siden alle notater kan inneholde undernotater. Ett notat kan legges inn flere steder i herarkiet.", + "attributes_title": "Notatetiketter og -relasjoner", + "hoisting_description": "Du kan enkelt skille personlige og arbeidsnotater ved å gruppere de under arbeidsrom, som fokuserer notat-treet ditt på kun ønskede notater." }, "productivity_benefits": { "sync_title": "Synkronisering", @@ -26,7 +31,12 @@ "protected_notes_title": "Beskyttede notater", "title": "Produktivitet og sikkerhet", "sync_content": "Bruk en selv-hostet eller cloud-instans for å enkelt synkronisere notater på tvers av enheter, og ha de tilgjengelige fra din mobiltelefon ved hjelp av progressiv web-app.", - "jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser." + "jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser.", + "revisions_content": "Notater lagres periodisk i bakgrunnen og revisjonshistorikk kan brukes for tilbakeblikk eller å omgjøre uønskede endringer. Revisjoner kan også lages manuelt.", + "protected_notes_content": "Beskytt sensitiv personlig informasjon ved å kryptere notater og låse de med en passordkryptert sesjon.", + "jump_to_title": "Hurtigsøk og kommandoer", + "search_content": "Eller søk etter tekst i notatene og finjuster søket ved å filtrere på foreldrenotat eller dybde.", + "web_clipper_content": "Hent nettsider (eller screenshots) og legg de direkte i Trilium ved hjelp av web clipper nettleserutvidelse." }, "note_types": { "canvas_title": "Kanvas", @@ -34,7 +44,14 @@ "text_title": "Tekstnotat", "code_title": "Kodenotat", "file_title": "Filnotat", - "mermaid_title": "Mermaid diagrammer" + "mermaid_title": "Mermaid diagrammer", + "title": "Flere måter å presentere informasjonen din", + "text_description": "Notatene redigeres med en visuell editor (WYSIWYG), som støtter tabeller, bilder, matematiske uttrykk og kodeblokker med syntaksutheving. Formater tekst hurtig med Markdown-inspirert syntaks eller \"slash-kommandoer\".", + "code_description": "Store samlinger med kildekode eller skript bruker en dedikert editor med syntaksfremheving for mange programmeringsspråk og med flere fargetema.", + "file_description": "Integrer multimediafiler som PDFer, bilder og video med forhåndsvisning i programmet.", + "mermaid_description": "Lag diagrammer som flytskjema, klasse- og sekvensdiagrammer, Ganttdiagrammer og mye mer ved hjelp av Mermaidsyntaks.", + "mindmap_description": "Organiser dine tanker visuelt eller gjør en brainstorming.", + "others_list": "og andre: <0>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 063/196] 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 e5ea9c498e..3148eb8b91 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 064/196] 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 3148eb8b91..fba7d6966e 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 065/196] 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 4146c0ca0e..00e6ce62d3 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 066/196] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=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 9ad41c5527..4da9a79f62 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 067/196] 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 c994dc43d1..6b5b8eba55 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 068/196] 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 6d3e6dfde5..81c56863a4 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 069/196] 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 d8d5c861db..b65d463022 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 070/196] 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 173b837592..c47ebdb119 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 071/196] 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 e8fa4dcc31..2900c5de56 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 072/196] 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 47c44269dc..3528cce111 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 073/196] 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 e13f37fcf5..252cce17f7 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 074/196] 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 c0ccea2ea9..c9bc04b7fa 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" From 8645d053dee14d284fb3afd953b52a37fe9b6a19 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 19:54:15 +0200 Subject: [PATCH 075/196] fix(client): ckeditor theme not loaded properly --- apps/client/src/desktop.html | 1 + apps/server/src/assets/views/desktop.ejs | 1 - apps/server/src/assets/views/mobile.ejs | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/client/src/desktop.html b/apps/client/src/desktop.html index d4b7b190ca..2488b622f7 100644 --- a/apps/client/src/desktop.html +++ b/apps/client/src/desktop.html @@ -48,6 +48,7 @@ function loadStylesheets() { const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob; const cssToLoad = []; + cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`); cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`); if (themeCssUrl) { cssToLoad.push(themeCssUrl); diff --git a/apps/server/src/assets/views/desktop.ejs b/apps/server/src/assets/views/desktop.ejs index 44e6ee0ff0..f58856985d 100644 --- a/apps/server/src/assets/views/desktop.ejs +++ b/apps/server/src/assets/views/desktop.ejs @@ -3,7 +3,6 @@ - diff --git a/apps/server/src/assets/views/mobile.ejs b/apps/server/src/assets/views/mobile.ejs index 6100bd0dc7..e329d92ff3 100644 --- a/apps/server/src/assets/views/mobile.ejs +++ b/apps/server/src/assets/views/mobile.ejs @@ -117,7 +117,6 @@ - <% if (themeCssUrl) { %> From cd64548299f27e9eb06e3159d4f450f30af8fc0d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 20:22:37 +0200 Subject: [PATCH 076/196] fix(client): load custom fonts --- apps/client/src/desktop.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/desktop.html b/apps/client/src/desktop.html index 2488b622f7..8d4f2f3159 100644 --- a/apps/client/src/desktop.html +++ b/apps/client/src/desktop.html @@ -49,6 +49,7 @@ const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob; const cssToLoad = []; cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`); + cssToLoad.push(`api/fonts`); cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`); if (themeCssUrl) { cssToLoad.push(themeCssUrl); From e47c848ec88dce28f025285ad8137e697c2bc68b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 20:26:41 +0200 Subject: [PATCH 077/196] chore(server): reintegrate mobile layout --- apps/client/src/{desktop.html => index.html} | 2 +- apps/server/src/assets/views/desktop.ejs | 11 -- apps/server/src/assets/views/mobile.ejs | 136 ------------------- apps/server/src/routes/assets.ts | 2 +- 4 files changed, 2 insertions(+), 149 deletions(-) rename apps/client/src/{desktop.html => index.html} (98%) delete mode 100644 apps/server/src/assets/views/desktop.ejs delete mode 100644 apps/server/src/assets/views/mobile.ejs diff --git a/apps/client/src/desktop.html b/apps/client/src/index.html similarity index 98% rename from apps/client/src/desktop.html rename to apps/client/src/index.html index 8d4f2f3159..4bd8a775a6 100644 --- a/apps/client/src/desktop.html +++ b/apps/client/src/index.html @@ -100,7 +100,7 @@ async function loadScripts() { const assetPath = glob.assetPath; await import(`./${assetPath}/runtime.js`); - await import(`./${assetPath}/desktop.js`); + await import(`./${assetPath}/${glob.device}.js`); } bootstrap(); diff --git a/apps/server/src/assets/views/desktop.ejs b/apps/server/src/assets/views/desktop.ejs deleted file mode 100644 index f58856985d..0000000000 --- a/apps/server/src/assets/views/desktop.ejs +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/apps/server/src/assets/views/mobile.ejs b/apps/server/src/assets/views/mobile.ejs deleted file mode 100644 index e329d92ff3..0000000000 --- a/apps/server/src/assets/views/mobile.ejs +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - - - - Trilium Notes - - - - - - - - - - -
- - - -<%- include("./partials/windowGlobal.ejs", locals) %> - - - - - -<% if (themeCssUrl) { %> - -<% } %> - -<% if (themeUseNextAsBase === "next") { %> - -<% } else if (themeUseNextAsBase === "next-dark") { %> - -<% } else if (themeUseNextAsBase === "next-light") { %> - -<% } %> - - - - - diff --git a/apps/server/src/routes/assets.ts b/apps/server/src/routes/assets.ts index d8a67d881d..36512398c9 100644 --- a/apps/server/src/routes/assets.ts +++ b/apps/server/src/routes/assets.ts @@ -38,7 +38,7 @@ async function register(app: express.Application) { // The page is restored from cache, but the API call fail. res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - req.url = `/${assetUrlFragment}/src/desktop.html`; + req.url = `/${assetUrlFragment}/src/index.html`; vite.middlewares(req, res, next); }); app.use(`/node_modules/@excalidraw/excalidraw/dist/prod`, persistentCacheStatic(path.join(srcRoot, "../../node_modules/@excalidraw/excalidraw/dist/prod"))); From 18d701525ebf94ca6dacd907759d59a0a3943e85 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 20:30:59 +0200 Subject: [PATCH 078/196] fix(client): print broken due to lack of query forwarding ; Conflicts: ; apps/client/src/index.html --- apps/client/src/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/index.html b/apps/client/src/index.html index 4bd8a775a6..0ef0cf29d6 100644 --- a/apps/client/src/index.html +++ b/apps/client/src/index.html @@ -35,7 +35,7 @@ } async function setupGlob() { - const response = await fetch("./bootstrap"); + const response = await fetch(`/bootstrap${window.location.search}`); const json = await response.json(); global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */ From ee6cbc710c74044ef4215d6ccd315df7488ce023 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 20:38:34 +0200 Subject: [PATCH 079/196] chore(server): remove font size globs --- apps/server/src/routes/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/server/src/routes/index.ts b/apps/server/src/routes/index.ts index da0174999f..7bc59411a0 100644 --- a/apps/server/src/routes/index.ts +++ b/apps/server/src/routes/index.ts @@ -44,10 +44,6 @@ export function bootstrap(req: Request, res: Response) { isElectron, hasNativeTitleBar: isElectron && nativeTitleBarVisible, hasBackgroundEffects: isElectron && isWindows11 && !nativeTitleBarVisible && options.backgroundEffects === "true", - // TODO: These font size don't actually seem to be used. - mainFontSize: parseInt(options.mainFontSize, 10), - treeFontSize: parseInt(options.treeFontSize, 10), - detailFontSize: parseInt(options.detailFontSize, 10), maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"), maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"), instanceName: config.General ? config.General.instanceName : null, From a3fc13de3a3b8ee4e3e697d2a668a96c09214c94 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 21:00:40 +0200 Subject: [PATCH 080/196] refactor(client): extract bootstrap script into separate file --- apps/client/src/index.html | 82 +------------------------------- apps/client/src/index.ts | 81 +++++++++++++++++++++++++++++++ apps/client/vite.config.mts | 3 +- apps/server/src/routes/assets.ts | 4 ++ 4 files changed, 87 insertions(+), 83 deletions(-) diff --git a/apps/client/src/index.html b/apps/client/src/index.html index 0ef0cf29d6..0a3e9dc0ee 100644 --- a/apps/client/src/index.html +++ b/apps/client/src/index.html @@ -24,87 +24,7 @@ - - + - diff --git a/apps/client/src/index.ts b/apps/client/src/index.ts index df5c6a8537..588ebe361d 100644 --- a/apps/client/src/index.ts +++ b/apps/client/src/index.ts @@ -1,9 +1,6 @@ -import $ from "jquery"; - async function bootstrap() { - (window as any).$ = $; - (window as any).jQuery = $; - + showSplash(); + await initJQuery(); await setupGlob(); await loadBootstrapCss(); loadStylesheets(); @@ -13,6 +10,12 @@ async function bootstrap() { hideSplash(); } +async function initJQuery() { + const $ = (await import("jquery")).default; + (window as any).$ = $; + (window as any).jQuery = $; +} + async function setupGlob() { const response = await fetch(`/bootstrap${window.location.search}`); const json = await response.json(); @@ -93,8 +96,13 @@ async function loadScripts() { } } +function showSplash() { + // hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide