import { Injectable } from '@angular/core' ; import { getElementOffset } from 'ngx-tethys/util' ; export interface CaretCoordinates { top : number ; left : number ; height : number ; } export interface CaretOptions { debug : boolean ; } const properties = [ 'direction' , 'boxSizing' , 'width' , 'height' , 'overflowX' , 'overflowY' , 'borderTopWidth' , 'borderRightWidth' , 'borderBottomWidth' , 'borderLeftWidth' , 'borderStyle' , 'paddingTop' , 'paddingRight' , 'paddingBottom' , 'paddingLeft' , 'fontStyle' , 'fontVariant' , 'fontWeight' , 'fontStretch' , 'fontSize' , 'fontSizeAdjust' , 'lineHeight' , 'fontFamily' , 'textAlign' , 'textTransform' , 'textIndent' , 'textDecoration' , 'letterSpacing' , 'wordSpacing' , 'tabSize' , 'MozTabSize' ] ; const isBrowser = typeof window !== 'undefined' ; const isFirefox = isBrowser && window [ 'mozInnerScreenX' ] != null ; export type InputOrTextAreaElement = HTMLInputElement | HTMLTextAreaElement ; export type AllElement = InputOrTextAreaElement | HTMLElement ; export class CaretPositioner { static getTextareaCaretCoordinates ( element , position , options ? ) { if ( ! isBrowser ) { throw new Error ( 'textarea-caret-position#getCaretCoordinates should only be called in a browser' ) ; } const debug = ( options && options . debug ) || false ; if ( debug ) { const el = document . querySelector ( '#input-textarea-caret-position-mirror-div' ) ; if ( el ) { el . parentNode . removeChild ( el ) ; } } const div = document . createElement ( 'div' ) ; div . id = 'input-textarea-caret-position-mirror-div' ; document . body . appendChild ( div ) ; const style = div . style ; const computed = window . getComputedStyle ? window . getComputedStyle ( element ) : element [ 'currentStyle' ] ; const isInput = element . nodeName === 'INPUT' ; style . whiteSpace = 'pre-wrap' ; if ( ! isInput ) { style . wordWrap = 'break-word' ; } style . position = 'absolute' ; if ( ! debug ) { style . visibility = 'hidden' ; } properties . forEach ( function ( prop ) { if ( isInput && prop === 'lineHeight' ) { if ( computed . boxSizing === 'border-box' ) { const height = parseInt ( computed . height , 10 ) ; const outerHeight = parseInt ( computed . paddingTop , 10 ) + parseInt ( computed . paddingBottom , 10 ) + parseInt ( computed . borderTopWidth , 10 ) + parseInt ( computed . borderBottomWidth , 10 ) ; const targetHeight = outerHeight + parseInt ( computed . lineHeight , 10 ) ; if ( height > targetHeight ) { style . lineHeight = height - outerHeight + 'px' ; } else if ( height === targetHeight ) { style . lineHeight = computed . lineHeight ; } else { style . lineHeight = '0' ; } } else { style . lineHeight = computed . height ; } } else { style [ prop ] = computed [ prop ] ; } } ) ; if ( isFirefox ) { if ( element . scrollHeight > parseInt ( computed . height , 10 ) ) { style . overflowY = 'scroll' ; } } else { style . overflow = 'hidden' ; } div . textContent = element . value . substring ( 0 , position ) ; if ( isInput ) { div . textContent = div . textContent . replace ( /\s/g , '\u00a0' ) ; } const span = document . createElement ( 'span' ) ; span . textContent = element . value . substring ( position ) || '.' ; div . appendChild ( span ) ; const coordinates = { top : span . offsetTop + parseInt ( computed [ 'borderTopWidth' ] , 10 ) , left : span . offsetLeft + parseInt ( computed [ 'borderLeftWidth' ] , 10 ) , height : parseInt ( computed [ 'lineHeight' ] , 10 ) } ; if ( debug ) { span . style . backgroundColor = '#aaa' ; } else { document . body . removeChild ( div ) ; } return coordinates ; } static getEditableCaretCoordinates ( element ) { if ( window . getSelection ( ) . rangeCount ) { const range = window . getSelection ( ) . getRangeAt ( 0 ) ; const rect = range . getBoundingClientRect ( ) ; let height = range . startContainer . nodeType === 1 ? getComputedStyle ( range . startContainer as Element ) . lineHeight : getComputedStyle ( range . startContainer . parentNode as Element ) . lineHeight ; if ( isNaN ( height as any ) ) { let node = range . startContainer as HTMLElement ; if ( range . startContainer . nodeType !== 1 ) { node = node . parentNode as HTMLElement ; } const current = node . style . lineHeight ; node . style . lineHeight = '1em' ; height = parseInt ( getComputedStyle ( node ) . lineHeight , 10 ) ; node . style . lineHeight = current != null ? current : '' ; if ( ! node . getAttribute ( 'style' ) . length ) { node . removeAttribute ( 'style' ) ; } } const editableRect = element . getBoundingClientRect ( ) ; return { top : rect . top - editableRect . top , left : rect . left - editableRect . left , height : height as number } ; } else { return { top : 0 , left : 0 , height : 0 } ; } } static getCaretCoordinates ( element , position , options ? ) { const isInput = [ 'INPUT' , 'TEXTAREA' ] . indexOf ( element . nodeName ) >= 0 ; if ( isInput ) { return this . getTextareaCaretCoordinates ( element as InputOrTextAreaElement , position , options ) ; } else { return this . getEditableCaretCoordinates ( element ) ; } } static getCaretPosition ( element , position , options ? ) { const coordinates = CaretPositioner . getCaretCoordinates ( element , position , options ) ; const elementOffset = getElementOffset ( element ) ; return { top : coordinates . top + elementOffset . top , left : coordinates . left + elementOffset . left } ; } }