import { CodeEditor , IEditorServices } from '@jupyterlab/codeeditor' ; import { ReactWidget , showDialog , Dialog , WidgetTracker , } from '@jupyterlab/apputils' ; import { Button } from '@jupyterlab/ui-components' ; import { Message } from '@lumino/messaging' ; import React from 'react' ; import { CodeSnippetService } from './CodeSnippetService' ; import { CodeSnippetWidget } from './CodeSnippetWidget' ; import { SUPPORTED_LANGUAGES } from './CodeSnippetLanguages' ; import { CodeSnippetEditorTags } from './CodeSnippetEditorTags' ; import { showMessage } from './CodeSnippetMessage' ; import { validateInputs , saveOverWriteFile } from './CodeSnippetUtilities' ; /**
 * CSS style classes
 */ const CODE_SNIPPET_EDITOR = 'jp-codeSnippet-editor' ; const CODE_SNIPPET_EDITOR_TITLE = 'jp-codeSnippet-editor-title' ; const CODE_SNIPPET_EDITOR_METADATA = 'jp-codeSnippet-editor-metadata' ; const CODE_SNIPPET_EDITOR_INPUT_ACTIVE = 'jp-codeSnippet-editor-active' ; const CODE_SNIPPET_EDITOR_NAME_INPUT = 'jp-codeSnippet-editor-name' ; const CODE_SNIPPET_EDITOR_LABEL = 'jp-codeSnippet-editor-label' ; const CODE_SNIPPET_EDITOR_DESC_INPUT = 'jp-codeSnippet-editor-description' ; const CODE_SNIPPET_EDITOR_LANG_INPUT = 'jp-codeSnippet-editor-language' ; const CODE_SNIPPET_EDITOR_MIRROR = 'jp-codeSnippetInput-editor' ; const CODE_SNIPPET_EDITOR_INPUTAREA = 'jp-codeSnippetInputArea' ; const CODE_SNIPPET_EDITOR_INPUTAREA_MIRROR = 'jp-codeSnippetInputArea-editor' ; const EDITOR_DIRTY_CLASS = 'jp-mod-dirty' ; export interface ICodeSnippetEditorMetadata { name : string ; description : string ; language : string ; code : string [ ] ; id : number ; selectedTags : string [ ] ; allSnippetTags : string [ ] ; allLangTags : string [ ] ; fromScratch : boolean ; } export class CodeSnippetEditor extends ReactWidget { editorServices : IEditorServices ; private editor : CodeEditor . IEditor ; private saved : boolean ; private oldCodeSnippetName : string ; private _codeSnippetEditorMetaData : ICodeSnippetEditorMetadata ; private _hasRefreshedSinceAttach : boolean ; contentsService : CodeSnippetService ; codeSnippetWidget : CodeSnippetWidget ; tracker : WidgetTracker ; constructor ( editorServices , tracker , codeSnippetWidget , args ) { super ( ) ; this . addClass ( CODE_SNIPPET_EDITOR ) ; this . contentsService = CodeSnippetService . getCodeSnippetService ( ) ; this . editorServices = editorServices ; this . tracker = tracker ; this . _codeSnippetEditorMetaData = args ; this . oldCodeSnippetName = args . name ; this . saved = true ; this . _hasRefreshedSinceAttach = false ; this . codeSnippetWidget = codeSnippetWidget ; this . renderCodeInput = this . renderCodeInput . bind ( this ) ; this . handleInputFieldChange = this . handleInputFieldChange . bind ( this ) ; this . activateCodeMirror = this . activateCodeMirror . bind ( this ) ; this . saveChange = this . saveChange . bind ( this ) ; this . updateSnippet = this . updateSnippet . bind ( this ) ; this . handleChangeOnTag = this . handleChangeOnTag . bind ( this ) ; } get codeSnippetEditorMetadata ( ) : ICodeSnippetEditorMetadata { return this . _codeSnippetEditorMetaData ; } private deactivateEditor ( event ) { let target = event . target as HTMLElement ; while ( target && target . parentElement ) { if ( target . classList . contains ( CODE_SNIPPET_EDITOR_MIRROR ) || target . classList . contains ( CODE_SNIPPET_EDITOR_NAME_INPUT ) || target . classList . contains ( CODE_SNIPPET_EDITOR_DESC_INPUT ) ) { break ; } target = target . parentElement ; } const nameInput = document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_NAME_INPUT }` ) ; const descriptionInput = document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_DESC_INPUT }` ) ; const editor = document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } #code-${ this . _codeSnippetEditorMetaData . id }` ) ; if ( target . classList . contains ( CODE_SNIPPET_EDITOR_NAME_INPUT ) ) { this . deactivateDescriptionField ( descriptionInput ) ; this . deactivateCodeMirror ( editor ) ; } else if ( target . classList . contains ( CODE_SNIPPET_EDITOR_DESC_INPUT ) ) { this . deactivateNameField ( nameInput ) ; this . deactivateCodeMirror ( editor ) ; } else if ( target . classList . contains ( CODE_SNIPPET_EDITOR_MIRROR ) ) { this . deactivateNameField ( nameInput ) ; this . deactivateDescriptionField ( descriptionInput ) ; } else { this . deactivateNameField ( nameInput ) ; this . deactivateDescriptionField ( descriptionInput ) ; this . deactivateCodeMirror ( editor ) ; } } private deactivateNameField ( nameInput ) { if ( nameInput . classList . contains ( CODE_SNIPPET_EDITOR_INPUT_ACTIVE ) ) { nameInput . classList . remove ( CODE_SNIPPET_EDITOR_INPUT_ACTIVE ) ; } } private deactivateDescriptionField ( descriptionInput ) { if ( descriptionInput . classList . contains ( CODE_SNIPPET_EDITOR_INPUT_ACTIVE ) ) { descriptionInput . classList . remove ( CODE_SNIPPET_EDITOR_INPUT_ACTIVE ) ; } } private activeFieldState ( event ) { const target = event . target as HTMLElement ; if ( ! target . classList . contains ( CODE_SNIPPET_EDITOR_INPUT_ACTIVE ) ) { target . classList . add ( CODE_SNIPPET_EDITOR_INPUT_ACTIVE ) ; } } onUpdateRequest ( msg ) { super . onUpdateRequest ( msg ) ; if ( ! this . editor && document . getElementById ( 'code-' + this . _codeSnippetEditorMetaData . id ) ) { const editorFactory = this . editorServices . factoryService . newInlineEditor ; const getMimeTypeByLanguage = this . editorServices . mimeTypeService . getMimeTypeByLanguage ; this . editor = editorFactory ( { host : document . getElementById ( 'code-' + this . _codeSnippetEditorMetaData . id ) , model : new CodeEditor . Model ( { value : this . _codeSnippetEditorMetaData . code . join ( '\n' ) , mimeType : getMimeTypeByLanguage ( { name : this . _codeSnippetEditorMetaData . language , codemirror_mode : this . _codeSnippetEditorMetaData . language , } ) , } ) , } ) ; this . editor . model . value . changed . connect ( ( args ) => { this . _codeSnippetEditorMetaData . code = args . text . split ( '\n' ) ; if ( ! this . title . className . includes ( EDITOR_DIRTY_CLASS ) ) { this . title . className += ` ${ EDITOR_DIRTY_CLASS }` ; } this . saved = false ; } ) ; } if ( this . isVisible ) { this . _hasRefreshedSinceAttach = true ; this . editor . refresh ( ) ; } } onAfterAttach ( msg ) { super . onAfterAttach ( msg ) ; this . _hasRefreshedSinceAttach = false ; if ( this . isVisible ) { this . update ( ) ; } window . addEventListener ( 'beforeunload' , ( e ) => { if ( ! this . saved ) { e . preventDefault ( ) ; e . returnValue = '' ; } } ) ; } onAfterShow ( msg ) { if ( ! this . _hasRefreshedSinceAttach ) { this . update ( ) ; } } param msg onActivateRequest ( msg ) { this . editor . focus ( ) ; } onCloseRequest ( msg ) { if ( ! this . saved ) { showDialog ( { title : 'Close without saving?' , body : ( < p > { ' ' } { `"${ this . _codeSnippetEditorMetaData . name }" has unsaved changes, close without saving?` } { ' ' } < /p>), buttons : [ Dialog . cancelButton ( ) , Dialog . warnButton ( { label : 'Discard' } ) , Dialog . okButton ( { label : 'Save' } ) , ] , } ) . then ( ( response ) : void => { if ( response . button . accept ) { if ( response . button . label === 'Discard' ) { this . dispose ( ) ; super . onCloseRequest ( msg ) ; } else if ( response . button . label === 'Save' ) { const name = ( document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_NAME_INPUT }` ) as HTMLInputElement ) . value ; const description = ( document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_DESC_INPUT }` ) as HTMLInputElement ) . value ; const language = ( document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_LANG_INPUT }` ) as HTMLSelectElement ) . value ; const validity = validateInputs ( name , description , language ) ; if ( validity ) { this . updateSnippet ( ) . then ( ( value ) => { if ( value ) { this . dispose ( ) ; super . onCloseRequest ( msg ) ; } } ) ; } } } } ) ; } else { this . dispose ( ) ; super . onCloseRequest ( msg ) ; } } param event activateCodeMirror ( event ) { let target = event . target as HTMLElement ; while ( target && target . parentElement ) { if ( target . classList . contains ( CODE_SNIPPET_EDITOR_MIRROR ) ) { break ; } target = target . parentElement ; } const editor = document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } #code-${ this . _codeSnippetEditorMetaData . id }` ) ; if ( target . classList . contains ( CODE_SNIPPET_EDITOR_MIRROR ) ) { if ( ! editor . classList . contains ( 'active' ) ) { editor . classList . add ( 'active' ) ; } } } deactivateCodeMirror ( editor ) { if ( editor . classList . contains ( 'active' ) ) { editor . classList . remove ( 'active' ) ; } } handleInputFieldChange ( event ) { if ( ! this . title . className . includes ( EDITOR_DIRTY_CLASS ) ) { this . title . className += ` ${ EDITOR_DIRTY_CLASS }` ; } const target = event . target as HTMLElement ; if ( ! target . classList . contains ( 'FieldChanged' ) ) { target . classList . add ( 'FieldChanged' ) ; } this . saved = false ; } saveChange ( event ) { const name = ( document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_NAME_INPUT }` ) as HTMLInputElement ) . value ; const description = ( document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_DESC_INPUT }` ) as HTMLInputElement ) . value ; const language = ( document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_LANG_INPUT }` ) as HTMLSelectElement ) . value ; const validity = validateInputs ( name , description , language ) ; if ( validity ) { this . updateSnippet ( ) ; } } async updateSnippet ( ) { const name = ( document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_NAME_INPUT }` ) as HTMLInputElement ) . value ; const description = ( document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_DESC_INPUT }` ) as HTMLInputElement ) . value ; const language = ( document . querySelector ( `.${ CODE_SNIPPET_EDITOR }-${ this . _codeSnippetEditorMetaData . id } .${ CODE_SNIPPET_EDITOR_LANG_INPUT }` ) as HTMLSelectElement ) . value ; this . _codeSnippetEditorMetaData . name = name ; this . _codeSnippetEditorMetaData . description = description ; this . _codeSnippetEditorMetaData . language = language ; const newName = this . _codeSnippetEditorMetaData . name ; const oldName = this . oldCodeSnippetName ; const newSnippet = { name : this . _codeSnippetEditorMetaData . name , description : this . _codeSnippetEditorMetaData . description , language : this . _codeSnippetEditorMetaData . language , code : this . _codeSnippetEditorMetaData . code , id : this . _codeSnippetEditorMetaData . id , tags : this . _codeSnippetEditorMetaData . selectedTags , } ; const isDuplicatName = this . contentsService . duplicateNameExists ( newName ) ; this . oldCodeSnippetName = this . _codeSnippetEditorMetaData . name ; if ( this . _codeSnippetEditorMetaData . fromScratch ) { if ( isDuplicatName ) { const oldSnippet = this . contentsService . getSnippet ( newName ) [ 0 ] ; await saveOverWriteFile ( this . contentsService , oldSnippet , newSnippet ) ; } else { this . contentsService . addSnippet ( newSnippet ) . then ( ( res ) => { if ( ! res ) { console . log ( 'Error in adding snippet' ) ; return false ; } } ) ; showMessage ( 'confirm' ) ; } } else { if ( newName !== oldName ) { if ( isDuplicatName ) { const oldSnippet = this . contentsService . getSnippet ( newName ) [ 0 ] ; await saveOverWriteFile ( this . contentsService , oldSnippet , newSnippet ) . then ( ( res ) => { if ( res ) { const removedSnippet = this . contentsService . getSnippet ( oldName ) [ 0 ] ; this . contentsService . deleteSnippet ( removedSnippet . id ) ; } else { return false ; } } ) ; } } this . contentsService . modifyExistingSnippet ( oldName , newSnippet ) . then ( ( res ) => { if ( ! res ) { console . log ( 'Error in modifying snippet' ) ; return false ; } } ) ; } this . saved = true ; this . title . className = this . title . className . replace ( ` ${ EDITOR_DIRTY_CLASS }` , '' ) ; this . title . label = '[' + this . _codeSnippetEditorMetaData . language + '] ' + this . _codeSnippetEditorMetaData . name ; if ( ! this . _codeSnippetEditorMetaData . fromScratch ) { this . tracker . save ( this ) ; } this . codeSnippetWidget . updateCodeSnippetWidget ( ) ; if ( this . _codeSnippetEditorMetaData . fromScratch ) { this . dispose ( ) ; } return true ; } handleChangeOnTag ( selectedTags , allTags ) { if ( ! this . title . className . includes ( EDITOR_DIRTY_CLASS ) ) { this . title . className += ` ${ EDITOR_DIRTY_CLASS }` ; } this . _codeSnippetEditorMetaData . selectedTags = selectedTags ; this . _codeSnippetEditorMetaData . allSnippetTags = allTags ; this . saved = false ; } handleOnBlur ( event ) { const target = event . target as HTMLElement ; if ( ! target . classList . contains ( 'touched' ) ) { target . classList . add ( 'touched' ) ; } } /**
     * TODO: clean CSS style class - "Use constant"
     */ renderCodeInput ( ) { return ( < section className = { CODE_SNIPPET_EDITOR_INPUTAREA_MIRROR } onMouseDown = { this . activateCodeMirror } > < div className = { CODE_SNIPPET_EDITOR_MIRROR } id = { 'code-' + this . _codeSnippetEditorMetaData . id . toString ( ) } > < /div> < /section>); } renderLanguages ( ) { SUPPORTED_LANGUAGES . sort ( ) ; return ( < div > < input className = { CODE_SNIPPET_EDITOR_LANG_INPUT } list = "languages" name = "language" defaultValue = { this . _codeSnippetEditorMetaData . language } onChange = { this . handleInputFieldChange } required / > < datalist id = "languages" > { SUPPORTED_LANGUAGES . map ( ( lang ) => this . renderLanguageOptions ( lang ) ) } < /datalist> < /div>); } renderLanguageOptions ( option ) { return < option key = { option } value = { option } / > ; } render ( ) { const fromScratch = this . _codeSnippetEditorMetaData . fromScratch ; return ( < div className = { CODE_SNIPPET_EDITOR_INPUTAREA } onMouseDown = { ( event ) : void => { this . deactivateEditor ( event ) ; } } > < span className = { CODE_SNIPPET_EDITOR_TITLE } > { fromScratch ? 'New Code Snippet' : 'Edit Code Snippet' } < /span> < section className = { CODE_SNIPPET_EDITOR_METADATA } > < label className = { CODE_SNIPPET_EDITOR_LABEL } > Name ( required ) < /label> < input className = { CODE_SNIPPET_EDITOR_NAME_INPUT } defaultValue = { this . _codeSnippetEditorMetaData . name } placeholder = { 'Ex. starter code' } type = "text" required onMouseDown = { ( event ) : void => this . activeFieldState ( event ) } onChange = { ( event ) : void => { this . handleInputFieldChange ( event ) ; } } onBlur = { this . handleOnBlur } > < /input> < label className = { CODE_SNIPPET_EDITOR_LABEL } > Description ( optional ) < /label> < input className = { CODE_SNIPPET_EDITOR_DESC_INPUT } defaultValue = { this . _codeSnippetEditorMetaData . description } placeholder = { 'Description' } type = "text" onMouseDown = { ( event ) : void => this . activeFieldState ( event ) } onChange = { ( event ) : void => { this . handleInputFieldChange ( event ) ; } } onBlur = { this . handleOnBlur } > < /input> < label className = { CODE_SNIPPET_EDITOR_LABEL } > Language ( required ) < /label> { this . renderLanguages ( ) } < label className = { CODE_SNIPPET_EDITOR_LABEL } > Tags < /label> < CodeSnippetEditorTags selectedTags = { this . codeSnippetEditorMetadata . selectedTags } snippetTags = { this . codeSnippetEditorMetadata . allSnippetTags } langTags = { this . codeSnippetEditorMetadata . allLangTags } handleChange = { this . handleChangeOnTag } / > < /section> < span className = { CODE_SNIPPET_EDITOR_LABEL } > Code < /span> { this . renderCodeInput ( ) } < Button className = "saveBtn" onClick = { this . saveChange } > { fromScratch ? 'Create & Close' : 'Save' } < /Button> < /div>); } }