import { formatTimestamp } from '@components/utils' ; import { textToLinkedText } from '@components/utils/Text' ; import { Tooltip } from '@material-ui/core' ; import { makeStyles } from '@material-ui/core/styles' ; import { CSSProperties } from '@material-ui/styles' ; import settings from '@models/api/Settings' ; import { compTextMessage , TextMessage , TextMessages } from '@models/ISharedContent' ; import { assert , findTextColorRGB } from '@models/utils' ; import { getRandomColorRGB , rgba } from '@models/utils' ; import _ from 'lodash' ; import { Observer , useObserver } from 'mobx-react-lite' ; import React , { useEffect , useRef } from 'react' ; import { ContentProps } from './Content' ; class TextMember { messages : TextMessage [ ] = [ ] ; isStatic = false ; abortScroll = false ; editing = false ; } function onUpdateTexts ( messages , div , props ) { const newTexts = { messages , scroll : [ div . scrollLeft , div . scrollTop ] } ; const newUrl = JSON . stringify ( newTexts ) ; if ( props . content . url !== newUrl && props . updateAndSend ) { props . content . url = newUrl ; props . updateAndSend ( props . content ) ; } } interface TextDivProps extends ContentProps { text : TextMessage ; textEditing : boolean ; textToShow : JSX . Element [ ] ; member : TextMember ; div : HTMLDivElement | null ; autoFocus : boolean ; } interface TextEditProps extends TextDivProps { css : React . CSSProperties ; } function sendText ( text , props ) { props . text . message = text ; const messagesToSend = props . member . messages . filter ( text => text . message . length ) ; if ( props . div ) { onUpdateTexts ( messagesToSend , props . div , props ) ; } } export const TextEdit = ( props ) => { const [ text , setText ] = React . useState ( props . text . message ) ; const sendTextLaterRef = useRef < any > ( undefined ) ; useEffect ( ( ) => { sendTextLaterRef . current = _ . throttle ( sendText , 1000 , { leading : false } ) ; } , [ ] ) ; const sendTextLater = sendTextLaterRef . current ; return < Observer > { ( ) => < div style = { { ... props . css , position : 'relative' , margin : 0 , border : 0 , padding : 0 , backgroundColor : 'none' } } > < div style = { { ... props . css , color : 'red' , position : 'relative' , width : '100%' , overflow : 'hidden' , visibility : 'hidden' } } > { text + '\u200b' } < /div> < textarea value = { text } autoFocus = { props . autoFocus } style = { { ... props . css , font : 'inherit' , verticalAlign : 'baseline' , resize : 'none' , position : 'absolute' , top : 0 , left : 0 , width : '100%' , height : '100%' , border : 'none' , letterSpacing : 'inherit' , overflow : 'hidden' } } onChange = { ( ev ) => { setText ( ev . currentTarget . value ) ; if ( sendTextLater ) { sendTextLater ( ev . currentTarget . value , props ) ; } } } onBlur = { ( ) => { if ( sendTextLater ) { sendTextLater . cancel ( ) ; } sendText ( text , props ) ; } } onFocus = { ( ) => { } } onKeyDown = { ( ev ) => { if ( ev . key === 'Escape' || ev . key === 'Esc' ) { ev . stopPropagation ( ) ; ev . preventDefault ( ) ; if ( sendTextLater ) { sendTextLater . cancel ( ) ; } sendText ( text , props ) ; props . stores . contents . setEditing ( '' ) ; } } } /> < /div>}</Observer > ; } ; export const TextDiv = ( props ) => { const timestamp = formatTimestamp ( props . text . time ) ; const rgb = props . text . color ?. length ? props . text . color : getRandomColorRGB ( props . text . name ) ; const textColor = props . text . textColor ?. length ? props . text . textColor : findTextColorRGB ( rgb ) ; const css = { color : rgba ( textColor , 1 ) , backgroundColor : settings . useTransparent ? rgba ( rgb , 0.5 ) : rgba ( rgb , 1 ) , padding : '0.1em' , fontSize : 16 , lineHeight : 1.2 , width : '100%' , overflow : 'clip' , boxSizing : 'border-box' , whiteSpace : 'pre-wrap' , wordWrap : 'break-word' , overflowWrap : 'break-word' , } ; const { backgroundColor , ... cssEdit } = css ; cssEdit . backgroundColor = rgba ( rgb , 1 ) ; return < Tooltip title = { < React . Fragment > { props . text . name } < br / > { timestamp } < /React.Fragment>} placement="left" arrow={true} suppressContentEditableWarning={true}><div> { props . textEditing ? < TextEdit { ... props } css = { cssEdit } / > : < div style = { css } onPointerDown = { ( ) => { props . member . isStatic = true ; } } onPointerMove = { ( ) => { props . member . isStatic = false ; } } onPointerUp = { ( ev ) => { if ( ! props . member . isStatic ) { return ; } const target = ev . target ; if ( target instanceof Node ) { ev . preventDefault ( ) ; const selection = window . getSelection ( ) ; if ( selection ) { if ( selection . rangeCount && selection . getRangeAt ( 0 ) . toString ( ) ) { selection . removeAllRanges ( ) ; } else { const range = document . createRange ( ) ; range . selectNode ( target ) ; selection . removeAllRanges ( ) ; selection . addRange ( range ) ; } } } } } > { props . textToShow } < /div>} < /div></Tooltip > ; } ; const cssText = { height : '100%' , width : '100%' , whiteSpace : 'pre-line' , pointerEvents : 'auto' , overflowY : 'auto' , overflowX : 'visible' , wordWrap : 'break-word' , } ; const useStyles = makeStyles ( { text : cssText , textEdit : { ... cssText , cursor : 'default' , userSelect : 'text' , } , } ) ; export const Text = ( props ) => { assert ( props . content . type === 'text' ) ; const { contents , participants } = props . stores ; const memberRef = React . useRef < TextMember > ( new TextMember ( ) ) ; const member = memberRef . current ; const classes = useStyles ( ) ; const ref = useRef < HTMLDivElement > ( null ) ; const url = props . content . url ; const newTexts = JSON . parse ( url ) as TextMessages ; const editing = useObserver ( ( ) => contents . editing === props . content . id ) ; if ( editing ) { contents . setBeforeChangeEditing ( ( cur , next ) => { if ( cur === props . content . id && next === '' ) { if ( ref . current ) { member . messages = member . messages . filter ( text => text . message . length ) ; onUpdateTexts ( member . messages , ref . current , props ) ; } contents . setBeforeChangeEditing ( ) ; } } ) ; } useEffect ( ( ) => { if ( ! editing && ref . current ) { if ( ref . current . scrollLeft !== newTexts . scroll [ 0 ] || ref . current . scrollTop !== newTexts . scroll [ 1 ] ) { member . abortScroll = true ; ref . current ?. scroll ( newTexts . scroll [ 0 ] , newTexts . scroll [ 1 ] ) ; } } } , [ newTexts . scroll [ 0 ] , newTexts . scroll [ 1 ] , editing ] ) ; const indices = new Set < number > ( ) ; newTexts . messages . forEach ( ( newMessage ) => { const index = member . messages . findIndex ( msg => msg . pid === newMessage . pid && msg . time === newMessage . time ) ; if ( index === - 1 ) { indices . add ( member . messages . length ) ; member . messages . push ( newMessage ) ; } else if ( newMessage . pid !== participants . localId ) { indices . add ( index ) ; member . messages [ index ] = newMessage ; } } ) ; member . messages = member . messages . filter ( ( msg , idx ) => msg . pid === participants . localId || indices . has ( idx ) ) ; member . messages . sort ( compTextMessage ) ; let focusToEdit = undefined ; if ( editing ) { const last = member . messages . pop ( ) ; if ( last ) { member . messages . push ( last ) ; } if ( last ?. pid !== participants . localId ) { member . messages . push ( { message : '' , pid : participants . localId , name : participants . local . information . name , color : participants . local . information . color , textColor : participants . local . information . textColor , time : Date . now ( ) } ) ; } if ( ! member . editing ) { member . messages . reverse ( ) ; focusToEdit = member . messages . find ( message => message . pid === participants . localId ) ; member . messages . reverse ( ) ; } } const textDivs = member . messages . map ( ( text , idx ) => { const textEditing = ( editing && ( text . pid === participants . localId || ! participants . remote . has ( text . pid ) ) ) ; let textToShow = [ ] ; if ( ! textEditing ) { textToShow = textToLinkedText ( text . message ) ; } return < TextDiv { ... props } key = { idx } text = { text } div = { ref . current } textToShow = { textToShow } member = { member } textEditing = { textEditing } autoFocus = { text === focusToEdit } />; } ) ; const INTERVAL = 200 ; const handleScroll = _ . debounce ( ( ev ) => { if ( ref . current ) { if ( member . abortScroll ) { member . abortScroll = false ; } else { onUpdateTexts ( member . messages , ref . current , props ) ; } } } , INTERVAL ) ; member . editing = editing ; return < div ref = { ref } className = { editing ? classes . textEdit : classes . text } onWheel = { ev => ev . ctrlKey || ev . stopPropagation ( ) } onScroll = { ( ev ) => { if ( ! editing ) { handleScroll ( ev ) ; } } } > { textDivs } < /div>; } ;