import Editor from '../Editor' ; import { getLineNodeEnd , getLineNodeStart , HTMLLineElement } from './rendering' ; import { isBRPlaceholder } from './html' ; import { createTreeWalker } from './walker' ; import { EditorRange } from '../doc/EditorRange' ; import { Types } from '../typesetting' ; import Line from '../doc/Line' ; type NodeAndOffset = [ Node | null , number ] ; type NodeOffsetAndFrozen = [ Node | null , number , boolean ? ] ; const EMPTY_NODE_OFFSET = [ null , 0 ] ; export interface LineInfo { line : Line ; element : HTMLLineElement ; rect : DOMRect ; belowMid : boolean ; } export function getIndexFromPoint ( editor , x , y ) { const document = editor . root . ownerDocument ; if ( 'caretPositionFromPoint' in document ) { try { const pos = ( document as any ) . caretPositionFromPoint ( x , y ) ; if ( pos ) { return getIndexFromNodeAndOffset ( editor , pos . offsetNode , pos . offset ) ; } } catch ( _ ) { } } if ( document . caretRangeFromPoint ) { const range = document . caretRangeFromPoint ( x , y ) as Range ; if ( range ) { return getIndexFromNodeAndOffset ( editor , range . startContainer , range . startOffset ) ; } } return null ; } export function getLineInfoFromPoint ( editor , y ) { const { root } = editor ; if ( ! root . ownerDocument ) return ; const lineElements = Array . from ( root . querySelectorAll ( editor . typeset . lines . selector ) ) . filter ( elem => ( elem as any ) . key ) as HTMLLineElement [ ] ; const last = lineElements [ lineElements . length - 1 ] ; for ( const element of lineElements ) { const rect = element . getBoundingClientRect ( ) ; if ( rect . bottom >= y || element === last ) { const line = editor . doc . getLineBy ( element . key ) ; return { line , element , rect , belowMid : y > rect . top + rect . height / 2 } ; } } } export function getBrowserRange ( editor , range ) { if ( range [ 0 ] > range [ 1 ] ) range = [ range [ 1 ] , range [ 0 ] ] ; const [ anchorNode , anchorOffset , focusNode , focusOffset ] = getNodesForRange ( editor , range ) ; const browserRange = editor . root . ownerDocument . createRange ( ) ; if ( anchorNode && focusNode ) { browserRange . setStart ( anchorNode , anchorOffset ) ; browserRange . setEnd ( focusNode , focusOffset ) ; } return browserRange ; } export function getBoudingBrowserRange ( editor , range ) { const browserRange = getBrowserRange ( editor , range ) ; if ( browserRange ?. endContainer . nodeType === Node . ELEMENT_NODE ) { try { browserRange . setEnd ( browserRange . endContainer , browserRange . endOffset + 1 ) ; } catch ( e ) { } } return browserRange ; } export function getIndexFromNodeAndOffset ( editor , node , offset , current ? ) { const { root } = editor ; const { lines } = editor . typeset ; if ( ! root . contains ( node ) ) { return - 1 ; } if ( node . nodeType === Node . ELEMENT_NODE ) { if ( node . childNodes . length === offset ) { if ( getLineNodeEnd ( root , node ) != null ) return getLineNodeEnd ( root , node ) - 1 ; if ( node . childNodes . length ) { node = node . childNodes [ offset - 1 ] ; offset = getNodeLength ( editor , node ) ; } } else { node = node . childNodes [ offset ] ; offset = 0 ; } const start = getLineNodeStart ( root , node ) ; if ( start != null ) { if ( lines . findByNode ( node ) ?. frozen ) { return start + offset ; } return ( current == null || current < start ? start : start - 1 ) + offset ; } } return getIndexFromNode ( editor , node ) + offset ; } export function getIndexFromNode ( editor , startNode ) { const { root } = editor ; if ( ! root . ownerDocument ) return - 1 ; const { lines , embeds } = editor . typeset ; const walker = createTreeWalker ( root ) ; walker . currentNode = startNode ; let node ; let index = 0 ; let start ; while ( ( node = walker . previousNode ( ) ) ) { if ( node === root ) break ; else if ( ( start = getLineNodeStart ( root , node ) ) != null ) { index += start ; break ; } else if ( node . nodeType === Node . TEXT_NODE ) index += textNodeLength ( lines , node ) ; else if ( ( node as HTMLElement ) . classList ?. contains ( 'decoration' ) ) index ; else if ( embeds . matches ( node ) && ! isBRPlaceholder ( editor , node as HTMLElement ) ) index ++ ; else if ( lines . matches ( node ) && editor . doc . lines [ 0 ] . id !== ( node as any ) . key ) index ++ ; } return index ; } export function getLineElementAt ( editor , index ) { const { root } = editor ; if ( ! root . ownerDocument ) return ; const childNodes = Array . from ( root . childNodes ) ; return childNodes . find ( ( line ) => getLineNodeStart ( root , line ) <= index && getLineNodeEnd ( root , line ) > index ) as HTMLLineElement ; } export function getNodeLength ( editor , parentNode ) { const { lines , embeds } = editor . typeset ; if ( embeds . matches ( parentNode ) && ! isBRPlaceholder ( editor , parentNode as HTMLElement ) ) { return 1 ; } if ( parentNode . nodeType === Node . TEXT_NODE ) return textNodeLength ( lines , parentNode ) ; const walker = createTreeWalker ( parentNode ) ; let length = lines . findByNode ( parentNode ) ? 1 : 0 , node ; while ( node = walker . nextNode ( ) ) { if ( node . nodeType === Node . TEXT_NODE ) length += textNodeLength ( lines , node ) ; else if ( ( node as HTMLElement ) . classList ?. contains ( 'decoration' ) ) length ; else if ( embeds . matches ( node ) && ! isBRPlaceholder ( editor , node as HTMLElement ) ) length ++ ; else if ( lines . matches ( node ) ) length ++ ; } return length ; } export function getNodesForRange ( editor , range ) { if ( range == null ) { return [ null , 0 , null , 0 ] ; } else { const anchorFirst = range [ 0 ] <= range [ 1 ] ; const direction = anchorFirst ? 1 : - 1 ; const isCollapsed = range [ 0 ] === range [ 1 ] ; const [ anchorNode , anchorOffset , frozen ] = getNodeAndOffset ( editor , range [ 0 ] , anchorFirst ? 0 : 1 ) ; const [ focusNode , focusOffset ] = isCollapsed && ! frozen ? [ anchorNode , anchorOffset ] : frozen && ( isCollapsed || range [ 1 ] - range [ 0 ] === direction * editor . doc . getLineAt ( range [ 0 ] ) . length ) ? [ anchorNode , anchorOffset + ( anchorFirst ? 1 : - 1 ) ] : getNodeAndOffset ( editor , range [ 1 ] , anchorFirst ? 1 : 0 ) ; return [ anchorNode , anchorOffset , focusNode , focusOffset ] ; } } export function getNodeAndOffset ( editor , index , direction ) { const { root } = editor ; if ( ! root . ownerDocument ) return EMPTY_NODE_OFFSET ; const { lines , embeds } = editor . typeset ; const childNodes = Array . from ( root . childNodes ) ; const line = getLineElementAt ( editor , index ) ; if ( ! line ) return EMPTY_NODE_OFFSET ; const type = lines . findByNode ( line , true ) ; if ( type . frozen ) { return [ line . parentNode , childNodes . indexOf ( line ) + direction , true ] ; } index -= getLineNodeStart ( root , line ) ; const atStart = ! index ; const walker = createTreeWalker ( line ) ; let node , firstLineSeen = false ; while ( ( node = walker . nextNode ( ) ) ) { if ( node . nodeType === Node . TEXT_NODE ) { const size = textNodeLength ( lines , node ) ; if ( index <= size ) return [ node , index ] ; index -= size ; } else if ( ( node as HTMLElement ) . classList ?. contains ( 'decoration' ) ) { } else if ( embeds . matches ( node ) && ! isBRPlaceholder ( editor , node as HTMLElement ) ) { const embed = embeds . findByNode ( node ) ; if ( ! embed || embed . fromDom === false ) { continue ; } index -= 1 ; if ( index <= 0 ) { const children = Array . from ( ( node . parentNode as HTMLElement ) . childNodes ) as Node [ ] ; return [ node . parentNode , children . indexOf ( node ) + 1 + index ] ; } } else if ( lines . matches ( node ) ) { if ( firstLineSeen ) index -= 1 ; else firstLineSeen = true ; if ( index === 0 ) { const first = walker . firstChild ( ) ; if ( first && first . nodeType === Node . TEXT_NODE ) { return [ first , 0 ] ; } else if ( first ) { const children = Array . from ( node . childNodes ) as Node [ ] ; return [ node , children . indexOf ( first ) ] ; } else { return [ node , 0 ] ; } } } } return atStart ? [ line , 0 ] : EMPTY_NODE_OFFSET ; } export function textNodeLength ( lines , node ) { const value = node . nodeValue || '' ; if ( value . trim ( ) || ! ( lines . matches ( node . previousSibling ) || lines . matches ( node . nextSibling ) ) ) { return value . length ; } return 0 ; }