import { register } from '../editor/commands' ; import type { ModelPrivate } from './model-private' ; import { Atom , BranchName } from '../core/atom' ; import { SubsupAtom } from '../core-atoms/subsup' ; import { move , skip } from './commands' ; import { isBrowser } from '../common/capabilities' ; import MathfieldElement from '../public/mathfield-element' ; export function moveAfterParent ( model ) { const previousPosition = model . position ; if ( ! model . at ( previousPosition ) . parent ) { model . announce ( 'plonk' ) ; return false ; } model . position = model . offsetOf ( model . at ( model . position ) . parent ! ) ; model . announce ( 'move' , previousPosition ) ; return true ; } function superscriptDepth ( model ) { let result = 0 ; let atom = model . at ( model . position ) ; let wasSuperscript = false ; while ( atom ) { if ( ! atom . hasEmptyBranch ( 'superscript' ) || ! atom . hasEmptyBranch ( 'subscript' ) ) { result += 1 ; } if ( ! atom . hasEmptyBranch ( 'superscript' ) ) { wasSuperscript = true ; } else if ( ! atom . hasEmptyBranch ( 'subscript' ) ) { wasSuperscript = false ; } atom = atom . parent ; } return wasSuperscript ? result : 0 ; } function subscriptDepth ( model ) { let result = 0 ; let atom = model . at ( model . position ) ; let wasSubscript = false ; while ( atom ) { if ( ! atom . hasEmptyBranch ( 'superscript' ) || ! atom . hasEmptyBranch ( 'subscript' ) ) { result += 1 ; } if ( ! atom . hasEmptyBranch ( 'superscript' ) ) { wasSubscript = false ; } else if ( ! atom . hasEmptyBranch ( 'subscript' ) ) { wasSubscript = true ; } atom = atom . parent ; } return wasSubscript ? result : 0 ; } /**
 * Switch the cursor to the superscript and select it. If there is no subscript
 * yet, create one.
 */ function moveToSuperscript ( model ) { model . collapseSelection ( ) ; if ( superscriptDepth ( model ) >= model . mathfield . options . scriptDepth [ 1 ] ) { model . announce ( 'plonk' ) ; return false ; } let target = model . at ( model . position ) ; if ( target . subsupPlacement === undefined ) { if ( target . rightSibling ?. type !== 'msubsup' ) { target . parent ! . addChildAfter ( new SubsupAtom ( { style : target . computedStyle } ) , target ) ; } target = target . rightSibling ; } target . createBranch ( 'superscript' ) ; model . setSelection ( model . getSiblingsRange ( model . offsetOf ( target . superscript ! [ 0 ] ) ) ) ; return true ; } /**
 * Switch the cursor to the subscript and select it. If there is no subscript
 * yet, create one.
 */ function moveToSubscript ( model ) { model . collapseSelection ( ) ; if ( subscriptDepth ( model ) >= model . mathfield . options . scriptDepth [ 0 ] ) { model . announce ( 'plonk' ) ; return false ; } let target = model . at ( model . position ) ; if ( target . subsupPlacement === undefined ) { if ( model . at ( model . position + 1 ) ?. type !== 'msubsup' ) { target . parent ! . addChildAfter ( new SubsupAtom ( { style : model . at ( model . position ) . computedStyle , } ) , target ) ; } target = model . at ( model . position + 1 ) ; } target . createBranch ( 'subscript' ) ; model . setSelection ( model . getSiblingsRange ( model . offsetOf ( target . subscript ! [ 0 ] ) ) ) ; return true ; } /**
 * Return an array of tabbable elements, approximately in the order a browser
 * would (the browsers are inconsistent), which is first by accounting
 * for non-null tabIndex, then null tabIndex, then document order of focusable
 * elements.
 */ function getTabbableElements ( ) { function tabbable ( element ) { const regularTabbables = [ ] ; const orderedTabbables = [ ] ; const candidates = [ ... element . querySelectorAll < HTMLElement > ( `input, select, textarea, a[href], button,
        [tabindex], audio[controls], video[controls],
        [contenteditable]:not([contenteditable="false"]), details>summary` ) , ] . filter ( isNodeMatchingSelectorTabbable ) ; candidates . forEach ( ( candidate , i ) => { const candidateTabindex = getTabindex ( candidate ) ; if ( candidateTabindex === 0 ) { regularTabbables . push ( candidate ) ; } else { orderedTabbables . push ( { documentOrder : i , tabIndex : candidateTabindex , node : candidate , } ) ; } } ) ; return orderedTabbables . sort ( ( a , b ) => a . tabIndex === b . tabIndex ? a . documentOrder - b . documentOrder : a . tabIndex - b . tabIndex ) . map ( ( a ) => a . node ) . concat ( regularTabbables ) ; } function isNodeMatchingSelectorTabbable ( element ) { if ( ! isNodeMatchingSelectorFocusable ( element ) || isNonTabbableRadio ( element ) || getTabindex ( element ) < 0 ) { return false ; } return true ; } function isNodeMatchingSelectorFocusable ( node ) { if ( node . disabled || ( node . type === 'hidden' && node . tagName . toUpperCase ( ) === 'INPUT' ) || isHidden ( node ) ) { return false ; } return true ; } function getTabindex ( node ) { const tabindexAttr = Number . parseInt ( node . getAttribute ( 'tabindex' ) ?? 'NaN' , 10 ) ; if ( ! Number . isNaN ( tabindexAttr ) ) { return tabindexAttr ; } if ( node . contentEditable === 'true' ) { return 0 ; } if ( ( node . nodeName === 'AUDIO' || node . nodeName === 'VIDEO' ) && node . getAttribute ( 'tabindex' ) === null ) { return 0 ; } return node . tabIndex ; } function isNonTabbableRadio ( node ) { return ( node . tagName . toUpperCase ( ) === 'INPUT' && ( node as HTMLInputElement ) . type === 'radio' && ! isTabbableRadio ( node as HTMLInputElement ) ) ; } function getCheckedRadio ( nodes , form ) { for ( const node of nodes ) { if ( node . checked && node . form === form ) { return node ; } } return null ; } function isTabbableRadio ( node ) { if ( ! node . name ) { return true ; } const radioScope = node . form ?? node . ownerDocument ; const radioSet = radioScope . querySelectorAll ( 'input[type="radio"][name="' + node . name + '"]' ) ; const checked = getCheckedRadio ( radioSet , node . form ) ; return ! checked || checked === node ; } function isHidden ( element ) { if ( ! isBrowser ( ) || element === document . activeElement || element . contains ( document . activeElement ) ) { return false ; } if ( getComputedStyle ( element ) . visibility === 'hidden' ) return true ; const bounds = element . getBoundingClientRect ( ) ; if ( bounds . width === 0 || bounds . height === 0 ) return true ; while ( element ) { if ( getComputedStyle ( element ) . display === 'none' ) return true ; element = element . parentElement ! ; } return false ; } if ( ! isBrowser ( ) ) return [ ] ; return tabbable ( document . body ) ; } return function leap ( model , dir , callHooks = true ) { const dist = dir === 'forward' ? 1 : - 1 ; if ( model . at ( model . anchor ) . type === 'placeholder' ) { move ( model , dir ) ; } const atoms = model . getAllAtoms ( Math . max ( model . position + dist , 0 ) ) ; if ( dir === 'backward' ) atoms . reverse ( ) ; const placeholders = atoms . filter ( ( atom ) => atom . type === 'placeholder' || ( atom . treeDepth > 2 && atom . isFirstSibling && atom . isLastSibling ) ) ; if ( placeholders . length === 0 ) { const handled = ! callHooks || ! model . hooks . tabOut ?. ( model , dir ) ; if ( handled ) { model . announce ( 'plonk' ) ; return false ; } const tabbable = getTabbableElements ( ) ; if ( ! document . activeElement || tabbable . length === 1 ) { model . announce ( 'plonk' ) ; return false ; } let index = tabbable . indexOf ( document . activeElement as HTMLElement ) + dist ; if ( document . activeElement instanceof MathfieldElement && moveToNextNestedMathfield ( document . activeElement as MathfieldElement , dir , dist ) ) { return true ; } if ( index < 0 ) index = tabbable . length - 1 ; if ( index >= tabbable . length ) index = 0 ; if ( tabbable [ index ] instanceof MathfieldElement && moveToNextNestedMathfield ( tabbable [ index ] as MathfieldElement , dir , dist ) ) { return true ; } tabbable [ index ] . focus ( ) ; if ( index === 0 ) { model . announce ( 'plonk' ) ; return false ; } return true ; } /**
     *
     */ function moveToNextNestedMathfield ( element , dir , dist ) { const nestedMathfield = [ ... ( element . shadowRoot ?. querySelectorAll < HTMLElement > ( 'math-field' ) ?? [ ] ) , ] ; if ( nestedMathfield . length ) { const activeMathfield = element . shadowRoot ?. activeElement ; console . log ( activeMathfield ) ; const activeIndex = nestedMathfield . indexOf ( activeMathfield as HTMLElement ) ; let newMathfieldIndex = activeIndex + dist ; console . log ( activeIndex ) ; if ( activeIndex < 0 && dir === 'backward' ) { newMathfieldIndex = nestedMathfield . length - 1 ; } if ( newMathfieldIndex >= 0 && newMathfieldIndex < nestedMathfield . length ) { console . log ( newMathfieldIndex , nestedMathfield , 'forward' ) ; nestedMathfield [ newMathfieldIndex ] . focus ( ) ; return true ; } } return false ; } const previousPosition = model . position ; const newPosition = model . offsetOf ( placeholders [ 0 ] ) ; if ( placeholders [ 0 ] . type === 'placeholder' ) { model . setSelection ( newPosition - 1 , newPosition ) ; } else { model . position = newPosition ; } model . announce ( 'move' , previousPosition ) ; return true ; } /**
 * If cursor is currently in:
 * - superscript: move to subscript, creating it if necessary
 * - subscript: move to superscript, creating it if necessary
 * - numerator: move to denominator
 * - denominator: move to numerator
 * - otherwise: move to superscript
 */ register ( { moveToOpposite : ( model ) : boolean => { const OPPOSITE_RELATIONS = { superscript : 'subscript' , subscript : 'superscript' , above : 'below' , below : 'above' , } ; const cursor = model . at ( model . position ) ; const { parent } = cursor ; if ( ! parent ) { model . announce ( 'plonk' ) ; return false ; } const relation = cursor . treeBranch ; let oppositeRelation ; if ( typeof relation === 'string' ) { oppositeRelation = OPPOSITE_RELATIONS [ relation ] ; } if ( ! oppositeRelation ) { if ( ! cursor . subsupPlacement ) { return moveToSuperscript ( model ) ; } return moveToSubscript ( model ) ; } if ( ! parent . branch ( oppositeRelation ) ) { parent . createBranch ( oppositeRelation ) ; } return model . setSelection ( model . getBranchRange ( model . offsetOf ( parent ) , oppositeRelation ) ) ; } , moveBeforeParent : ( model ) : boolean => { const { parent } = model . at ( model . position ) ; if ( ! parent ) { model . announce ( 'plonk' ) ; return false ; } model . position = model . offsetOf ( parent ) ; return true ; } , moveAfterParent : ( model ) : boolean => moveAfterParent ( model ) , moveToNextPlaceholder : ( model ) : boolean => leap ( model , 'forward' ) , moveToPreviousPlaceholder : ( model ) : boolean => leap ( model , 'backward' ) , moveToNextChar : ( model ) : boolean => move ( model , 'forward' ) , moveToPreviousChar : ( model ) : boolean => move ( model , 'backward' ) , moveUp : ( model ) : boolean => move ( model , 'upward' ) , moveDown : ( model ) : boolean => move ( model , 'downward' ) , moveToNextWord : ( model ) : boolean => skip ( model , 'forward' ) , moveToPreviousWord : ( model ) : boolean => skip ( model , 'backward' ) , moveToGroupStart : ( model ) : boolean => { const pos = model . offsetOf ( model . at ( model . position ) . firstSibling ) ; if ( pos === model . position ) { model . announce ( 'plonk' ) ; return false ; } model . position = pos ; return true ; } , moveToGroupEnd : ( model ) : boolean => { const pos = model . offsetOf ( model . at ( model . position ) . lastSibling ) ; if ( pos === model . position ) { model . announce ( 'plonk' ) ; return false ; } model . position = pos ; return true ; } , moveToMathFieldStart : ( model ) : boolean => { if ( model . position === 0 ) { model . announce ( 'plonk' ) ; return false ; } model . position = 0 ; return true ; } , moveToMathFieldEnd : ( model ) : boolean => { if ( model . position === model . lastOffset ) { model . announce ( 'plonk' ) ; return false ; } model . position = model . lastOffset ; return true ; } , moveToSuperscript : ( model ) : boolean => moveToSuperscript ( model ) , moveToSubscript : ( model ) : boolean => moveToSubscript ( model ) , } , { target : 'model' , category : 'selection-anchor' } ) ;