import { AfterViewInit , Component , QueryList , ViewChild , ViewChildren } from '@angular/core' ; import { ExtensionService } from './classes/extension.service' ; import { Card , CardOutput } from 'neuron-ipe-types' ; import { RegexService } from './classes/regex.service' ; /**
 * Manages the entire app in the Output Panel.
 */ @ Component ( { selector : 'app-root' , templateUrl : './app.component.html' , styleUrls : [ './app.component.css' ] , host : { '(window:resize)' : 'onWindowResize()' } } ) export class AppComponent implements AfterViewInit { /**
     * Array of all Cards.
     */ cards : Card [ ] = [ ] ; /**
     * Status of Selecting. True if user is selecting (in Select Mode).
     */ isSelecting = false ; /**
     * Set of Selected Cards.
     */ selectedCards = new Set < Card > ( ) ; /**
     * Map of Cards and there visibility.
     */ visibleCards = new Map < Card , boolean > ( ) ; /**
     * Search query in search box.
     */ searchQuery = '' ; /**
     * Status of Filters
     */ typeFilters = { text : true , rich : true , error : true } ; /**
     * Indicates if the Undo Snackbar is displayed.
     */ showingUndoButton : boolean = false ; /**
     * String to be displayed on Undo Snackbar
     */ undoContent : string = 'Card deleted' ; /**
     * Timer for Undo Snackbar
     */ undoButtonTimer = null ; constructor ( private extension , private regexService ) { extension . onAddCard . subscribe ( card => { this . addCard ( card ) ; } ) ; } param cards showUndoButton ( cards ) { if ( cards == 1 ) { this . undoContent = `Card deleted` ; } else { this . undoContent = `${ cards } cards deleted` ; } this . showingUndoButton = true ; if ( this . undoButtonTimer ) { clearTimeout ( this . undoButtonTimer ) ; } this . undoButtonTimer = setTimeout ( ( ) => { this . undoButtonTimer = null ; this . showingUndoButton = false ; } , 10000 ) ; } param filters updateFilters ( filters ) { this . typeFilters = filters ; this . checkVisible ( ) ; } param search updateSearch ( search ) { this . searchQuery = search ; this . checkVisible ( ) ; } param card returns cardMatchesSearchQuery ( card ) { if ( this . searchQuery === '' ) { return true ; } let regexResult = this . regexService . regexQuery ( this . searchQuery ) ; if ( regexResult ) { if ( regexResult . test ( card . title ) || regexResult . test ( card . sourceCode ) ) { return true ; } } else { let pattern = this . searchQuery . replace ( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/gi , '\\$&' ) ; if ( new RegExp ( pattern , 'gi' ) . test ( card . title ) || new RegExp ( pattern , 'gi' ) . test ( card . sourceCode ) ) { return true ; } } return false ; } param card returns cardMatchesFilter ( card ) { if ( card . outputs && card . outputs . length === 0 ) return this . typeFilters . text ; for ( let output of card . outputs ) { if ( output . type == 'stdout' || output . type == 'text/plain' ) { if ( this . typeFilters . text ) return true ; } else if ( output . type == 'error' ) { if ( this . typeFilters . error ) return true ; } else { if ( this . typeFilters . rich ) return true ; } } return false ; } /**
     * Updates the map visibleCards so that only cards matching searcg and filters are displayed.
     */ checkVisible ( ) { for ( let card of this . cards ) { this . visibleCards . set ( card , this . cardMatchesFilter ( card ) && this . cardMatchesSearchQuery ( card ) ) ; } } param card cardSelected ( card ) { if ( this . selectedCards . has ( card ) ) { this . selectedCards . delete ( card ) ; } else { this . selectedCards . add ( card ) ; } } /**
     * Toggles isSelecting (Selection Mode).
     * Resets selectedCards if toggling off.
     */ updateSelecting ( ) { this . isSelecting = ! this . isSelecting ; if ( ! this . isSelecting ) { this . selectedCards = new Set < Card > ( ) ; } } /**
     * Deletes cards in the selectedCards set.
     */ deleteSelectedCards ( ) { let indexes = [ ] ; this . selectedCards . forEach ( value => { const index = this . cards . indexOf ( value ) ; if ( index > - 1 ) { indexes . push ( index ) ; this . cards . splice ( index , 1 ) ; } } ) ; this . extension . deleteSelectedCards ( indexes ) ; this . showUndoButton ( indexes . length ) ; } /**
     * Selects all cards and adds them to selectedCards.
     * If selectedCards already includes all cards then selectedCards is reset.
     */ selectAll ( ) { if ( this . selectedCards . size == this . cards . length ) { this . selectedCards = new Set < Card > ( ) ; } else { this . selectedCards = new Set < Card > ( this . cards ) ; } } param card param direction cardMoved ( card , direction ) { if ( direction === "up" ) this . moveUp ( card ) ; else if ( direction === "down" ) this . moveDown ( card ) ; } param card moveUp ( card ) { const index = this . cards . indexOf ( card , 1 ) ; if ( index > - 1 ) { const tmp = this . cards [ index - 1 ] ; this . cards [ index - 1 ] = this . cards [ index ] ; this . cards [ index ] = tmp ; this . extension . moveCardUp ( index ) ; } } param card moveDown ( card ) { const index = this . cards . indexOf ( card ) ; if ( index > - 1 && index < this . cards . length - 1 ) { const tmp = this . cards [ index + 1 ] ; this . cards [ index + 1 ] = this . cards [ index ] ; this . cards [ index ] = tmp ; this . extension . moveCardDown ( index ) ; } } /**
     * Exports cards.
     * If isSelecting is true only exports cards in selectedCards.
     */ export ( ) { let indexes = null ; if ( this . isSelecting ) { indexes = this . cards . filter ( card => this . selectedCards . has ( card ) ) . map ( ( card , index ) => index ) ; } this . extension . jupyterExport ( indexes ) ; } param pdf savePdf ( pdf ) { this . extension . savePdf ( pdf ) ; } param card openBrowser ( card ) { const index = this . cards . indexOf ( card ) ; this . extension . openInBrowser ( index ) ; } param card addCard ( card ) { this . cards . push ( card ) ; this . scrollToBottom ( ) ; } param card deleteCard ( card ) { const index = this . cards . indexOf ( card ) ; if ( index > - 1 ) { this . cards . splice ( index , 1 ) ; this . extension . deleteCard ( index ) ; this . showUndoButton ( 1 ) ; } } /**
     * Tell the backend that the outputCollapsed property of a card has changed.
     */ collapseOutput ( card , value ) { const index = this . cards . indexOf ( card ) ; this . extension . collapseOutput ( index , value ) ; } /**
     * Tell the backend that the codeCollapsed property of a card has changed.
     */ collapseCode ( card , value ) { const index = this . cards . indexOf ( card ) ; this . extension . collapseCode ( index , value ) ; } /**
     * Tell the backend that the collapsed property of a card has changed.
     */ collapseCard ( card , value ) { const index = this . cards . indexOf ( card ) ; this . extension . collapseCard ( index , value ) ; } /**
     * Tell the backend that the title property of a card has changed.
     */ changeTitle ( card , newTitle ) { const index = this . cards . indexOf ( card ) ; this . extension . changeCardTitle ( index , newTitle ) ; } /**
     * Sync an entire card with the backend by sending its contents in full.
     * This is useful when editing markdown cards from the frontend, for example.
     */ editCustomCard ( card ) { const index = this . cards . indexOf ( card ) ; this . extension . editCustomCard ( index , card ) ; } /**
     * Tell the backend that the undo button was clicked.
     */ undoClicked ( ) { this . extension . undoClicked ( ) ; this . showingUndoButton = false ; } private windowResizeThrottle ; /**
     * Called when the onresize DOM event is fired.
     * It reloads plotly graphs to make sure that they resize appropriately.
     */ onWindowResize ( ) { clearTimeout ( this . windowResizeThrottle ) ; this . windowResizeThrottle = setTimeout ( ( ) => { for ( let card of this . cards ) { for ( let output of card . outputs ) { if ( output . type == 'text/html' || output . type == 'application/vnd.plotly.v1+json' ) { const o = output . output ; output . output = '' ; setTimeout ( ( ) => { output . output = o ; } ) ; } } } } , 400 ) ; } /**
     * Creates a blank markdown-only card and appends it to the array
     */ newMarkdownCard ( ) { let markdownCard = new Card ( 0 , '' , '*Click to edit markdown*' , [ ] , { } , '' ) ; markdownCard . isCustomMarkdown = true ; this . cards . push ( markdownCard ) ; this . extension . addCustomCard ( markdownCard ) ; this . scrollToBottom ( ) ; } @ ViewChild ( 'scrollingList' ) scrollContainer ; /**
     * Scroll the cards list to the bottom.
     */ scrollToBottom ( ) { setTimeout ( ( ) => { try { this . scrollContainer . nativeElement . scrollTop = this . scrollContainer . nativeElement . scrollHeight ; } catch ( err ) { } } , 0 ) ; } ngAfterViewInit ( ) { this . scrollToBottom ( ) ; } }