import type { Selector } from '../public/commands' ; import { Atom } from '../core/atom-class' ; import { parseLatex } from '../core/parser' ; import { mightProducePrintableCharacter , eventToChar , } from '../editor/keyboard' ; import { getInlineShortcutsStartingWith , getInlineShortcut , } from '../editor/shortcuts' ; import { getCommandForKeybinding } from '../editor/keybindings' ; import { splitGraphemes } from '../core/grapheme-splitter' ; import { HAPTIC_FEEDBACK_DURATION , SelectorPrivate } from '../editor/commands' ; import { updateAutocomplete } from './autocomplete' ; import { requestUpdate } from './render' ; import type { MathfieldPrivate } from './mathfield-private' ; import { removeIsolatedSpace , smartMode } from './smartmode' ; import { getActiveKeyboardLayout , validateKeyboardLayout , } from '../editor/keyboard-layout' ; import { ParseMode , Style } from '../public/core' ; import { moveAfterParent } from '../editor-model/commands-move' ; import { range } from '../editor-model/selection-utils' ; import { insertSmartFence } from './mode-editor-math' ; import { ModeEditor } from './mode-editor' ; import { effectiveMode } from '../editor/options' ; import { canVibrate } from '../common/capabilities' ; import { showKeystroke } from './keystroke-caption' ; param evt export function onKeystroke ( mathfield , keystroke , evt ? ) { const { model } = mathfield ; validateKeyboardLayout ( evt ) ; const activeLayout = getActiveKeyboardLayout ( ) ; if ( mathfield . keyboardLayout !== activeLayout . id ) { mathfield . keyboardLayout = activeLayout . id ; mathfield . _keybindings = undefined ; } showKeystroke ( mathfield , keystroke ) ; clearTimeout ( mathfield . keystrokeBufferResetTimer ) ; if ( ! mathfield . options . readOnly && mathfield . options . onKeystroke && ! mathfield . options . onKeystroke ( mathfield , keystroke , evt ) ) { if ( evt ?. preventDefault ) { evt . preventDefault ( ) ; evt . stopPropagation ( ) ; } return false ; } let shortcut ; let stateIndex ; let selector = '' ; let resetKeystrokeBuffer = false ; if ( mathfield . mode !== 'latex' && ( ! evt || ( ! evt . ctrlKey && ! evt . metaKey ) ) ) { if ( keystroke === '[Backspace]' ) { mathfield . keystrokeBuffer = mathfield . keystrokeBuffer . slice ( 0 , - 1 ) ; mathfield . keystrokeBufferStates . pop ( ) ; mathfield . resetKeystrokeBuffer ( { defer : true } ) ; } else if ( evt && ! mightProducePrintableCharacter ( evt ) ) { mathfield . resetKeystrokeBuffer ( ) ; } else { const c = eventToChar ( evt ) ; const candidate = mathfield . keystrokeBuffer + c ; let i = 0 ; while ( ! shortcut && i < candidate . length ) { const context = mathfield . keystrokeBufferStates [ i ] ? parseLatex ( mathfield . keystrokeBufferStates [ i ] . latex , { parseMode : effectiveMode ( mathfield . options ) , macros : mathfield . options . macros , } ) : model . getAtoms ( model . offsetOf ( model . at ( model . position ) . firstSibling ) , model . position ) ; shortcut = getInlineShortcut ( context , candidate . slice ( i ) , mathfield . options . inlineShortcuts ) ; i += 1 ; } stateIndex = mathfield . keystrokeBufferStates . length - ( candidate . length - i ) ; mathfield . keystrokeBuffer += c ; mathfield . keystrokeBufferStates . push ( mathfield . getUndoRecord ( ) ) ; if ( getInlineShortcutsStartingWith ( candidate , mathfield . options ) . length <= 1 ) { resetKeystrokeBuffer = true ; } else { mathfield . resetKeystrokeBuffer ( { defer : true } ) ; } } } if ( mathfield . options . smartMode ) { const previousMode = mathfield . mode ; if ( shortcut ) { mathfield . mode = 'math' ; } else if ( smartMode ( mathfield , keystroke , evt ) ) { mathfield . mode = { math : 'text' , text : 'math' } [ mathfield . mode ] ; selector = '' ; } if ( mathfield . mode !== previousMode && typeof mathfield . options . onModeChange === 'function' ) { mathfield . options . onModeChange ( mathfield , mathfield . mode ) ; } } if ( ! shortcut && ! selector ) { selector = getCommandForKeybinding ( mathfield . keybindings , mathfield . mode , keystroke ) ; } if ( ! shortcut && ! selector && ( keystroke === '[Enter]' || keystroke === '[Return]' ) ) { if ( typeof mathfield . options . onCommit === 'function' ) { mathfield . options . onCommit ( mathfield ) ; if ( evt ?. preventDefault ) { evt . preventDefault ( ) ; evt . stopPropagation ( ) ; } return false ; } } if ( ! shortcut && ! selector ) { return true ; } const child = model . at ( Math . max ( model . position , model . anchor ) ) ; const { parent } = child ; if ( selector === 'moveAfterParent' && parent && parent . type === 'leftright' && child . isLastSibling && mathfield . options . smartFence && insertSmartFence ( model , '.' , mathfield . style ) ) { selector = '' ; requestUpdate ( mathfield ) ; } if ( mathfield . mode === 'math' && keystroke === '[Space]' && ! shortcut ) { if ( mathfield . options . mathModeSpace ) { mathfield . snapshot ( ) ; ModeEditor . insert ( 'math' , model , mathfield . options . mathModeSpace ) ; selector = '' ; mathfield . dirty = true ; } else { const nextSibling = model . at ( model . position + 1 ) ; const previousSibling = model . at ( model . position - 1 ) ; if ( ( nextSibling && nextSibling . mode === 'text' ) || ( previousSibling && previousSibling . mode === 'text' ) ) { mathfield . snapshot ( ) ; ModeEditor . insert ( 'text' , model , ' ' ) ; mathfield . dirty = true ; } } } if ( selector ) { mathfield . executeCommand ( selector ) ; } else if ( shortcut ) { mathfield . keyboardDelegate ! . cancelComposition ( ) ; const style = { ... model . at ( model . position ) . computedStyle , ... mathfield . style , } ; if ( ! /^(\\{|\\}|\\[|\\]|\\@|\\#|\\$|\\%|\\^|\\_|\\backslash)$/ . test ( shortcut ) ) { const saveMode = mathfield . mode ; ModeEditor . insert ( mathfield . mode , model , eventToChar ( evt ) , { suppressChangeNotifications : true , style , } ) ; mathfield . snapshot ( ) ; mathfield . restoreToUndoRecord ( mathfield . keystrokeBufferStates [ stateIndex ! ] ) ; mathfield . mode = saveMode ; } model . deferNotifications ( { content : true , selection : true } , ( ) : boolean => { ModeEditor . insert ( mathfield . mode , model , shortcut ! , { format : 'latex' , style , smartFence : true , } ) ; removeIsolatedSpace ( mathfield . model ) ; if ( shortcut ! . endsWith ( ' ' ) ) { mathfield . mode = 'text' ; ModeEditor . insert ( 'text' , model , ' ' , { style } ) ; } return true ; } ) ; mathfield . snapshot ( ) ; mathfield . dirty = true ; model . announce ( 'replacement' ) ; if ( resetKeystrokeBuffer ) { mathfield . resetKeystrokeBuffer ( ) ; } } mathfield . scrollIntoView ( ) ; if ( evt ?. preventDefault ) { evt . preventDefault ( ) ; evt . stopPropagation ( ) ; } return false ; } param options param { boolean } options . focus param { boolean } options . feedback param { boolean } options . simulateKeystroke private export function onTypedText ( mathfield , text , options ? ) { const { model } = mathfield ; if ( mathfield . options . readOnly ) { model . announce ( 'plonk' ) ; return ; } options = options ?? { } ; if ( options . focus ) { mathfield . focus ( ) ; } if ( options . feedback ) { if ( mathfield . options . keypressVibration && canVibrate ( ) ) { navigator . vibrate ( HAPTIC_FEEDBACK_DURATION ) ; } mathfield . keypressSound ?. play ( ) . catch ( console . warn ) ; } if ( typeof options . mode === 'string' && mathfield . mode !== options . mode ) { mathfield . switchMode ( options . mode ) ; } if ( options . simulateKeystroke ) { const c = text . charAt ( 0 ) ; const ev = new KeyboardEvent ( 'keypress' , { key : c } ) ; if ( ! onKeystroke ( mathfield , c , ev ) ) { return ; } } const style = { ... model . at ( model . position ) . computedStyle , ... mathfield . style , } ; if ( ! model . selectionIsCollapsed ) { model . position = model . deleteAtoms ( range ( model . selection ) ) ; mathfield . snapshot ( ) ; } const graphemes = splitGraphemes ( text ) ; if ( mathfield . mode === 'latex' ) { model . deferNotifications ( { content : true , selection : true } , ( ) => { for ( const c of graphemes ) { ModeEditor . insert ( 'latex' , model , c ) ; } updateAutocomplete ( mathfield ) ; } ) ; } else if ( mathfield . mode === 'text' ) { for ( const c of graphemes ) { ModeEditor . insert ( 'text' , model , c , { style } ) ; } } else if ( mathfield . mode === 'math' ) { for ( const c of graphemes ) { let selector = ( { '^' : 'moveToSuperscript' , '_' : 'moveToSubscript' , ' ' : 'moveAfterParent' , } as const ) [ c ] ; if ( c === ' ' && mathfield . options . mathModeSpace ) { selector = [ 'insert' , mathfield . options . mathModeSpace ] ; } if ( selector ) { mathfield . executeCommand ( selector ) ; } else if ( /\d/ . test ( c ) && mathfield . options . smartSuperscript && model . at ( model . position ) . treeBranch === 'superscript' && model . at ( model . position ) . hasNoSiblings ) { ModeEditor . insert ( 'math' , model , c , { style } ) ; moveAfterParent ( model ) ; } else { ModeEditor . insert ( 'math' , model , c , { style , smartFence : mathfield . options . smartFence , } ) ; } } } mathfield . snapshotAndCoalesce ( ) ; mathfield . dirty = true ; mathfield . scrollIntoView ( ) ; }