import { Component , OnInit , OnDestroy } from '@angular/core' ; import { ActivatedRoute } from '@angular/router' ; import { BackendService , AppState , JetpadModalService } from '../core/services' ; import { ErrorModalComponent , AlertModalComponent } from "../share/components" ; import { EditorModule } from './index' ; import { ShareModalComponent } from './components/share-modal' ; import { Comment } from './comment' ; declare let swellrt ; declare let window ; declare let document ; @ Component ( { selector : 'jp-editor' , templateUrl : 'editor.component.html' } ) export class EditorComponent implements OnInit , OnDestroy { private appStateSubscription : any ; private readonly STYLE_LINK : string = "link" ; private readonly TOP_BAR_OFFSET : number = 114 ; private docid : string ; private doc : any ; private editor : any ; private selectionStyles : any = { } ; private headers : Array < any > = new Array < any > ( ) ; private visibleLinkModal : boolean = false ; private inputLinkModal : any ; private linkRange : any ; private visibleContextMenu : boolean = false ; private visibleLinkContextMenu : boolean = false ; private caretPos : any = { x : 0 , y : 0 } ; private caretPosNode : any ; private connectionHandler : Function ; private status : string ; private profilesManager : any ; private profilesHandler : any ; private participantSessionsRecent : Array < any > = [ ] ; private participantSessionsPast : Array < any > = [ ] ; private participantSessionMe : any = { session : { id : null , online : false } , profile : { name : "(Loading...)" , shortName : "(Loading...)" , imageUrl : null , color : { cssColor : "rgb(255, 255, 255)" } , setName : function ( n ) { this . name = n ; } } } ; private newCommentSelection : any ; private selectedComment : Comment ; private commentsAction : string = "none" ; private commentsData : any ; private comments : Array < any > = new Array < any > ( ) ; private selectedCommentIndex : number ; private showCanvasCover : boolean = false ; private errorModal : any = null ; private shareModal : any = null ; private alertModal : any = null ; private rightPanelContent : string = "contributors" ; private currentSelection : any ; constructor ( private appState , private backend , private modalService , private route ) { } private static getSelectionStyles ( editor , range ) { return editor . getAnnotation ( [ 'paragraph/' , 'style/' , 'link' ] , range ) ; } private static getCommentAnnotation ( editor , range ) { let ants = editor . getAnnotation ( [ 'comment' ] , range ) ; return ants ? ants . comment : undefined ; } initAnnotations ( ) { let that = this ; this . backend . get ( ) . then ( service => { swellrt . Editor . AnnotationRegistry . define ( "@mark" , "mark" , { } ) ; swellrt . Editor . AnnotationRegistry . define ( "comment" , "comment" , { } ) ; swellrt . Editor . AnnotationRegistry . setHandler ( "header" , ( type , annot , event ) => { if ( swellrt . Annotation . EVENT_MOUSE != type ) { that . refreshHeadings ( ) ; } } ) ; swellrt . Editor . AnnotationRegistry . setHandler ( "comment" , ( type , annot , event ) => { if ( swellrt . Annotation . EVENT_ADDED == type ) { Comment . setOpen ( annot . value , that . commentsData ) ; that . refreshComments ( ) ; } if ( swellrt . Annotation . EVENT_REMOVED == type ) { that . refreshComments ( ) ; } } ) ; } ) ; } private sortParticipantSessions ( ) { this . participantSessionsPast = this . participantSessionsPast . sort ( ( a , b ) => { return b . session . lastActivityTime - a . session . lastActivityTime ; } ) ; } private setProfilesHandler ( ) { this . profilesHandler = { onLoaded : ( profileSession ) => { if ( profileSession . profile . isCurrentSessionProfile ( ) ) { return ; } let participantSession = { session : profileSession , profile : profileSession . profile } ; if ( profileSession . online ) this . participantSessionsRecent . unshift ( participantSession ) ; else this . participantSessionsPast . unshift ( participantSession ) ; } , onUpdated : ( profile ) => { } , onOffline : ( profileSession ) => { let participantSessionIndex = this . participantSessionsRecent . findIndex ( ( item ) => { return item . session . id == profileSession . id ; } ) ; if ( participantSessionIndex >= 0 ) { this . participantSessionsRecent . splice ( participantSessionIndex , 1 ) ; this . participantSessionsPast . unshift ( { session : profileSession , profile : profileSession . profile } ) ; } } , onOnline : ( profileSession ) => { let participantSessionIndex = this . participantSessionsPast . findIndex ( ( item ) => { return item . session . id == profileSession . id ; } ) ; if ( participantSessionIndex >= 0 ) { this . participantSessionsPast . splice ( participantSessionIndex , 1 ) ; this . participantSessionsRecent . unshift ( { session : profileSession , profile : profileSession . profile } ) ; } } } ; this . profilesManager . addStatusHandler ( this . profilesHandler ) ; this . profilesManager . enableStatusEvents ( true ) ; } private onMenuAction ( actionEvent ) { if ( actionEvent . event == "share" ) { this . showModalShare ( ) ; } else if ( actionEvent . event == "comments" ) { this . refreshComments ( ) ; if ( this . comments && this . comments . length > 0 ) { this . pickComment ( this . comments [ 0 ] ) ; this . rightPanelContent = actionEvent . event ; } } else if ( actionEvent . event == "contributors" ) { this . rightPanelContent = actionEvent . event ; } else if ( actionEvent . event == "comment-event" ) { this . onCommentEvent ( actionEvent . data ) ; } } private showModalShare ( ) { if ( this . shareModal ) { this . shareModal . destroy ( ) ; this . shareModal = undefined ; } let modal$ = this . modalService . create ( EditorModule , ShareModalComponent , { ok : ( ) => { this . shareModal . destroy ( ) ; this . shareModal = undefined ; } } ) ; modal$ . subscribe ( ( modal ) => { this . shareModal = modal ; } ) ; } private showModalError ( error ) { if ( this . errorModal ) { this . errorModal . destroy ( ) ; this . errorModal = undefined ; } let modal$ = this . modalService . create ( EditorModule , ErrorModalComponent , { message : error , ok : ( ) => { this . errorModal . destroy ( ) ; this . errorModal = undefined ; } } ) ; modal$ . subscribe ( ( modal ) => { this . errorModal = modal ; } ) ; } private showModalAlert ( msg ) { if ( this . alertModal ) { this . alertModal . destroy ( ) ; this . alertModal = undefined ; } let modal$ = this . modalService . create ( EditorModule , AlertModalComponent , { message : msg , ok : ( ) => { this . alertModal . destroy ( ) ; this . alertModal = undefined ; } } ) ; modal$ . subscribe ( ( modal ) => { this . alertModal = modal ; } ) ; } public ngOnInit ( ) { this . appStateSubscription = this . appState . subject . subscribe ( ( state ) => { if ( state . error ) { this . showModalError ( state . error ) ; } } ) ; this . initAnnotations ( ) ; window . onscroll = ( ) => { this . closeFloatingViews ( ) ; } ; this . backend . get ( ) . then ( s => { this . connectionHandler = ( status , error ) => { this . status = status ; if ( status == "ERROR" ) { let errorInfo = "Server Disconnected" ; if ( error && error . statusMessange ) errorInfo += ": " + error . statusMessage ; this . appState . set ( "error" , errorInfo ) ; } if ( status == "ERROR" || status == "TURBULENCE" || status == "DISCONNECTED" ) { } if ( status == "CONNECTED" ) { } } ; s . addConnectionHandler ( this . connectionHandler ) ; this . profilesManager = s . profilesManager ; this . setProfilesHandler ( ) ; this . editor = swellrt . Editor . createWithId ( "canvas-container" , s ) ; window . _editor = this . editor ; this . editor . setSelectionHandler ( ( range , editor , selection ) => { this . closeFloatingViews ( ) ; if ( selection ) { this . currentSelection = selection ; window . _selection = this . currentSelection ; } else { this . currentSelection = undefined ; } this . newCommentSelection = undefined ; if ( selection && selection . anchorPosition ) { this . caretPos . x = selection . anchorPosition . left ; this . caretPos . y = selection . anchorPosition . top ; } if ( selection && selection . focusNode ) { let focusParent = selection . focusNode . parentElement ; if ( focusParent . getBoundingClientRect ) { let rect = focusParent . getBoundingClientRect ( ) ; if ( rect . top > ( window . innerHeight - this . TOP_BAR_OFFSET ) ) { focusParent . scrollIntoView ( ) ; } } } if ( selection && selection . range ) { this . selectionStyles = EditorComponent . getSelectionStyles ( editor , selection . range ) ; if ( this . selectionStyles . link ) { this . visibleLinkContextMenu = true ; } else if ( ! selection . range . isCollapsed ( ) ) { this . visibleContextMenu = true ; } this . pickComment ( EditorComponent . getCommentAnnotation ( this . editor , selection . range ) ) ; } } ) ; this . route . params . subscribe ( ( param ) => { this . open ( param [ 'id' ] ) ; } ) ; if ( this . editor . checkBrowserCompat ( ) == "readonly" ) { this . showModalAlert ( "Sorry, this browser is not fully compatible yet. You can keep using Jetpad in read only mode." ) ; } if ( this . editor . checkBrowserCompat ( ) == "none" ) { this . showModalAlert ( "Sorry, this browser is not compatible." ) ; } } ) ; } public ngOnDestroy ( ) { this . appStateSubscription . dispose ( ) ; this . backend . get ( ) . then ( s => { s . removeConnectionHandler ( this . connectionHandler ) ; } ) ; if ( this . editor ) { this . editor . clean ( ) ; } if ( this . doc ) { this . backend . close ( this . docid ) ; } } refreshHeadings ( ) { this . headers = this . editor . getAnnotation ( [ "header" ] , swellrt . Editor . Range . ALL , true ) [ "header" ] ; } refreshComments ( ) { this . comments = this . editor . seekTextAnnotations ( "comment" , swellrt . Editor . Range . ALL ) [ "comment" ] ; } closeFloatingViews ( ) { this . visibleLinkModal = false ; this . visibleContextMenu = false ; this . visibleLinkContextMenu = false ; } open ( id ) { this . editor . clean ( ) ; this . backend . open ( id ) . then ( r => { this . docid = id ; this . doc = r . controller ; BackendService . initDocObject ( this . doc , this . docid ) ; this . commentsData = this . doc . get ( "comments" ) ; this . showCanvasCover = this . doc . get ( "text" ) . isEmpty ( ) ; this . editor . set ( this . doc . get ( "text" ) ) ; this . editor . edit ( true ) ; this . refreshHeadings ( ) ; this . refreshComments ( ) ; this . participantSessionMe = { session : this . profilesManager . getSession ( this . profilesManager . getCurrentSessionId ( ) , this . profilesManager . getCurrentParticipantId ( ) ) , profile : this . profilesManager . getCurrentProfile ( ) } ; this . sortParticipantSessions ( ) ; window . _doc = this . doc ; } ) . catch ( error => { this . appState . set ( "error" , "Error opening document " + id ) ; } ) ; } editStyle ( event ) { let selection = this . editor . getSelection ( ) ; if ( ! selection || ! selection . range ) return ; let range = selection . range ; if ( range . isCollapsed ( ) ) { if ( this . selectionStyles [ event . name ] ) { range = this . selectionStyles [ event . name ] . range ; } } if ( event . value ) { this . editor . setAnnotation ( event . name , event . value , range ) ; } else { this . editor . clearAnnotation ( event . name , range ) ; } this . selectionStyles = EditorComponent . getSelectionStyles ( this . editor , range ) ; } showModalLink ( ) { let selection = this . editor . getSelection ( ) ; if ( selection ) this . linkRange = selection . range ; else return ; if ( ! this . linkRange ) return ; this . closeFloatingViews ( ) ; let selectionLink = this . selectionStyles [ this . STYLE_LINK ] ; if ( selectionLink ) { this . linkRange = selectionLink . range ; this . inputLinkModal = { text : this . selectionStyles [ this . STYLE_LINK ] . text , url : this . selectionStyles [ this . STYLE_LINK ] . value } ; } else { let isText = ! this . linkRange . isCollapsed ( ) ; let text = isText ? this . editor . getText ( this . linkRange ) : "" ; let url = text . startsWith ( "http" ) ? text : "http://" + text ; this . inputLinkModal = { text : text , url : url } ; } this . visibleLinkModal = true ; } editLink ( link ) { this . visibleLinkModal = false ; let selectionLink = this . selectionStyles [ this . STYLE_LINK ] ; if ( ! link ) return ; if ( link . url && ! link . text ) link . text = link . url ; let toDelete = ! link . url ; if ( selectionLink ) { if ( toDelete ) { selectionLink . clear ( ) ; return ; } if ( selectionLink . value != link . url ) selectionLink . update ( link . url ) ; if ( selectionLink . text != link . text ) selectionLink . mutate ( link . text ) ; } else if ( this . linkRange ) { if ( link . text . length > 0 && link . url ) { let newRange = this . doc . get ( "text" ) . replace ( this . linkRange , link . text ) ; if ( newRange ) this . editor . setAnnotation ( this . STYLE_LINK , link . url , newRange ) ; } } this . linkRange = null ; this . inputLinkModal = null ; } private linkContextAction ( action ) { this . visibleLinkContextMenu = false ; if ( "edit" == action ) { this . showModalLink ( ) ; } if ( "delete" == action ) { this . editLink ( { text : "" } ) ; } } private contextAction ( action ) { this . visibleContextMenu = false ; if ( "link" == action ) { this . showModalLink ( ) ; } if ( "bookmark" == action ) { this . showModalAlert ( "Bookmarks will be available very soon." ) ; } if ( "comment" == action ) { setTimeout ( ( ) => { this . createComment ( ) ; } ) ; } } private createComment ( ) { this . selectedComment = undefined ; let selection = this . editor . getSelection ( ) ; let text = this . editor . getText ( selection . range ) ; if ( text . replace ( " " , "" ) . length > 0 ) { this . newCommentSelection = selection ; this . newCommentSelection . text = text ; this . commentsAction = "new" ; this . rightPanelContent = "comments" ; } } private pickComment ( commentAnnotation ) { if ( this . selectedComment ) { this . selectedComment . highlight ( false ) ; } if ( ! commentAnnotation ) { return ; } let values = commentAnnotation . value . split ( "," ) ; let comment = commentAnnotation ? Comment . get ( values [ values . length - 1 ] , commentAnnotation , this . participantSessionMe , this . editor , this . commentsData ) : undefined ; if ( comment ) { this . selectedComment = comment ; this . resetCommentIndex ( comment . id ) ; this . selectedComment . highlight ( true ) ; this . commentsAction = "edit" ; this . rightPanelContent = "comments" ; } else { this . selectedComment = undefined ; this . resetCommentIndex ( null ) ; this . commentsAction = "none" ; } } public resetCommentIndex ( commentId ) { this . selectedCommentIndex = 0 ; if ( commentId ) { for ( let i = 0 ; i < this . comments . length ; i ++ ) { if ( this . comments [ i ] . value == commentId ) { this . selectedCommentIndex = i ; return ; } } } } public onCommentEvent ( event ) { if ( event . type == "create" ) { this . selectedComment = Comment . create ( event . selection . range , event . text , this . participantSessionMe , this . editor , this . commentsData ) ; this . selectedComment . highlight ( true ) ; this . commentsAction = "edit" ; } else if ( event . type == "next" ) { let halt = false ; let commentId = this . comments [ this . selectedCommentIndex ] . value ; if ( ! commentId ) return ; while ( commentId == this . comments [ this . selectedCommentIndex ] . value && ! halt ) { this . selectedCommentIndex ++ ; if ( this . selectedCommentIndex >= this . comments . length ) { this . selectedCommentIndex = 0 ; halt = true ; } } this . pickComment ( this . comments [ this . selectedCommentIndex ] ) ; } else if ( event . type == "prev" ) { let halt = false ; let commentId = this . comments [ this . selectedCommentIndex ] . value ; if ( ! commentId ) return ; while ( commentId == this . comments [ this . selectedCommentIndex ] . value && ! halt ) { this . selectedCommentIndex -- ; if ( this . selectedCommentIndex < 0 ) { this . selectedCommentIndex = this . comments . length - 1 ; halt = true ; } } this . pickComment ( this . comments [ this . selectedCommentIndex ] ) ; } else if ( event . type == "focus" ) { let element = document . querySelector ( '[data-comment="' + this . selectedComment . id + '"]' ) ; element . scrollIntoView ( false ) ; } else if ( event . type == "close" ) { this . commentsAction = "none" ; this . rightPanelContent = "" ; } } private onCoverEvent ( event ) { } private onSwitchDiffHighlight ( event ) { if ( event ) { this . doc . get ( "text" ) . showDiffHighlight ( ) ; } else { this . doc . get ( "text" ) . hideDiffHighlight ( ) ; } } }