import { Component , Input , Output , ViewChild , OnInit , Renderer2 , EventEmitter , forwardRef , AfterViewInit , ViewEncapsulation , ChangeDetectionStrategy } from '@angular/core' ; import { NG_VALUE_ACCESSOR , ControlValueAccessor } from '@angular/forms' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import CodeMirror from 'codemirror' ; import 'codemirror/mode/yaml/yaml.js' ; import 'codemirror/mode/python/python.js' ; import 'codemirror/mode/powershell/powershell.js' ; import 'codemirror/mode/javascript/javascript.js' ; import 'codemirror/mode/htmlmixed/htmlmixed.js' ; import 'codemirror/mode/spreadsheet/spreadsheet.js' ; import 'codemirror/mode/handlebars/handlebars.js' ; import './mustache' ; import 'codemirror/addon/lint/lint.js' ; import 'codemirror/addon/search/search.js' ; import 'codemirror/addon/search/searchcursor.js' ; import 'codemirror/addon/search/jump-to-line.js' ; import 'codemirror/addon/dialog/dialog.js' ; import 'codemirror/addon/fold/foldcode.js' ; import 'codemirror/addon/fold/foldgutter.js' ; import 'codemirror/addon/fold/indent-fold.js' ; import 'codemirror/addon/hint/show-hint.js' ; import 'codemirror/addon/mode/overlay.js' ; import { HintCompletion } from './hint-completion.interface' ; const CODEMIRROR_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => CodeEditorComponent ) , multi : true } ; @ Component ( { exportAs : 'ngxCodemirror' , selector : 'ngx-codemirror' , providers : [ CODEMIRROR_VALUE_ACCESSOR ] , host : { class : 'ngx-codemirror' } , templateUrl : './code-editor.component.html' , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , styleUrls : [ './codemirror.css' , './lint.css' , './dialog.css' , './foldgutter.css' , './dracula.css' , './hint.scss' , './code-editor.component.scss' ] } ) export class CodeEditorComponent implements OnInit , AfterViewInit , ControlValueAccessor { @ Input ( ) config : any = { lineWrapping : true } ; @ Input ( ) theme = 'dracula' ; @ Input ( ) readOnly : string | boolean = false ; @ Input ( ) allowDropFileTypes : string [ ] = [ ] ; @ Input ( ) gutters : Array < string | { className : string ; style ? : string ; } > = [ ] ; @ Input ( ) mode ? : any ; @ Input ( ) lint ? : any ; @ Input ( ) autocompleteTokens ? : Array < string | HintCompletion > ; @ Input ( ) get autofocus ( ) { return this . _autofocus ; } set autofocus ( autofocus ) { this . _autofocus = coerceBooleanProperty ( autofocus ) ; } @ Input ( ) get lineNumbers ( ) { return this . _lineNumbers ; } set lineNumbers ( lineNumbers ) { this . _lineNumbers = coerceBooleanProperty ( lineNumbers ) ; } @ Output ( ) change : EventEmitter < any > = new EventEmitter ( ) ; @ Output ( ) blur : EventEmitter < any > = new EventEmitter ( ) ; @ ViewChild ( 'host' , { static : true } ) host : any ; @ ViewChild ( 'content' , { static : true } ) content : any ; instance : CodeMirror . EditorFromTextArea ; _value : string ; get value ( ) : string { return this . _value ; } set value ( val ) { if ( val !== this . _value ) { this . _value = val ; this . onChangeCallback ( val ) ; this . change . emit ( this . _value ) ; } } private _autofocus = false ; private _lineNumbers = false ; constructor ( private readonly renderer ) { } ngOnInit ( ) { this . config = { theme : this . theme , readOnly : this . readOnly , mode : this . mode , autofocus : this . autofocus , lint : this . lint , allowDropFileTypes : this . allowDropFileTypes , lineNumbers : this . lineNumbers , gutters : this . gutters , extraKeys : { 'Ctrl-Space' : 'autocomplete' } , ... this . config } ; if ( this . autocompleteTokens ) { this . config . hintOptions = this . config . hintOptions || { } ; this . config . hintOptions . hint = this . autocomplete . bind ( this ) ; } } ngAfterViewInit ( ) { if ( typeof this . value !== 'string' ) { const elm = this . content . nativeElement ; const code = elm . innerHTML ; for ( const childNode of elm . childNodes ) { this . renderer . removeChild ( elm , childNode ) ; } this . host . nativeElement . value = this . cleanCode ( code ) ; } this . instance = CodeMirror . fromTextArea ( this . host . nativeElement , this . config ) ; this . instance . on ( 'change' , this . onChange . bind ( this ) ) ; this . instance . on ( 'blur' , this . onBlur . bind ( this ) ) ; if ( this . autocompleteTokens ) { this . instance . on ( 'keyup' , this . onKeyUp . bind ( this ) ) ; } } cleanCode ( code ) { let lines = code . split ( '\n' ) ; lines = lines . filter ( function ( line ) { return line . trim ( ) . length > 0 ; } ) ; if ( ! lines . length ) return '' ; const firstLineWhitespace = lines [ 0 ] . match ( /^\s*/ ) [ 0 ] ; const startingWhitespaceRegex = new RegExp ( '^' + firstLineWhitespace ) ; lines = lines . map ( function ( line ) { return line . replace ( '=""' , '' ) . replace ( startingWhitespaceRegex , '' ) . replace ( /\s+$/ , '' ) ; } ) ; const codeToParse = lines . join ( '\n' ) . replace ( /\{ \{/gi , '{{' ) . replace ( /\} \}/gi , '}}' ) . replace ( /&lt;/gi , '<' ) . replace ( /&gt;/gi , '>' ) ; return codeToParse ; } onVisible ( ) { this . instance . refresh ( ) ; } onKeyUp ( cm , event ) { if ( ( ! cm . state . completionActive && event . keyCode > 64 && event . keyCode < 91 ) || event . keyCode === 219 ) { ( CodeMirror . commands as any ) . autocomplete ( cm , null , { completeSingle : false } ) ; } } onChange ( ) { this . updateValue ( this . instance . getValue ( ) ) ; } onBlur ( ) { this . blur . emit ( this . instance . getValue ( ) ) ; } updateValue ( value ) { this . value = value ; this . onTouchedCallback ( ) ; this . onChangeCallback ( value ) ; this . change . emit ( value ) ; } writeValue ( val ) { if ( val !== this . value && this . instance ) { this . _value = val ; this . instance . setValue ( this . _value ) ; } } registerOnChange ( fn ) { this . onChangeCallback = fn ; } registerOnTouched ( fn ) { this . onTouchedCallback = fn ; } private onTouchedCallback : ( ) => void = ( ) => { } ; private onChangeCallback : ( _ : any ) => void = ( ) => { } ; private autocomplete ( editor ) { const word = /[\S$]+/ ; const cur = editor . getCursor ( ) ; const curLine = editor . getLine ( cur . line ) ; const end = cur . ch ; let start = end ; while ( start && word . test ( curLine . charAt ( start - 1 ) ) ) { -- start ; } const curWord = start !== end && curLine . slice ( start , end ) ; const list = this . autocompleteTokens . filter ( ( s ) => { s = typeof s === 'string' ? s : s . text ; return s ? s . startsWith ( curWord ) : false ; } ) ; return { list , from : CodeMirror . Pos ( cur . line , start ) , to : CodeMirror . Pos ( cur . line , end ) } ; } }