import { nDigits } from '../../../utils/misc' ; import { elt , removeChildren , on , off } from '../../../utils/dom' ; import { StyleSpan , COLORS } from '../../style-map' ; import { View , ViewOptions , Viewport } from '../index' ; import ViewController from '../../view-controller' ; import LineCache , { Line } from '../../line-cache' ; import FontMetrics from './font-metrics' ; /**
 * Represents a line in the DOM editor.
 */ class LineNode { public element : HTMLPreElement ; private asciiWidth : number ; private lineHeight : number ; constructor ( public line , private metrics ) { this . asciiWidth = this . metrics . asciiWidth ( ) ; this . lineHeight = this . metrics . lineHeight ( ) ; this . element = ( < HTMLPreElement > elt ( 'pre' , null , null , `
      position: relative;
      border-width: 0;
      border-radius: 0;
      word-wrap: normal;
      margin: 0;
      z-index: 2;
      font-variant-ligatures: contextual;
      font: ${ this . metrics . fontString ( ) };
    ` ) ) ; line . cursors . forEach ( ( char ) => { this . element . appendChild ( this . createCursor ( char ) ) ; } ) ; line . styles . forEach ( ( styleSpan ) => { this . element . appendChild ( this . nodeFromStyleSpan ( styleSpan ) ) ; } ) ; } createCursor ( char ) { return elt ( 'div' , null , null , `position: absolute;
                                   border: 1px solid ${ COLORS . CURSOR };
                                   height: ${ this . lineHeight }px;
                                   left: ${ this . asciiWidth * char }px` ) ; } nodeFromStyleSpan ( styleSpan ) { const { style , range : { start , length } } = styleSpan ; const css = `
      font: ${ style . fontString ( this . metrics ) };
      color: ${ style . fg };
    ` ; return elt ( 'span' , this . line . text . substr ( start , length ) , null , css ) ; } } /**
 * A view built using the Document Object Model.
 * The scrolling mechanism takes inspiration from CodeMirror.
 * See http://marijnhaverbeke.nl/blog/a-pathological-scrolling-model.html
 *
 * See the Canvas implementation of a view for commented/documented code abotu
 * how a custom view class should be implemeneted.
 */ export default class DOMView implements View { private scroller : HTMLElement ; private wrapper : HTMLElement ; private gutter : HTMLElement ; private sizer : HTMLElement ; private lines : HTMLElement ; private mover : HTMLElement ; private metrics : FontMetrics ; private lineCache : LineCache ; private drawGutter : boolean = true ; private gutterChars : number = 0 ; private gutterWidth : number = 0 ; private gutterPadding : [ number , number ] = [ 20 , 0 ] ; constructor ( private controller , opts ) { this . wrapper = controller . wrapper ; this . lineCache = controller . lineCache ; this . metrics = new FontMetrics ( this . wrapper , { family : 'monospace' , size : 20 } ) ; [ 'drawGutter' , 'gutterPadding' , 'editorPadding' , 'scrollPastEnd' ] . forEach ( ( key ) => { if ( opts [ key ] ) { this [ key ] = opts [ key ] ; } } ) ; this . wrapper . style . background = COLORS . BACKGROUND ; this . wrapper . style . overflow = 'hidden' ; this . lines = elt ( 'div' , null , 'lines' , `
      min-height: 1px;
      cursor: text;
    ` ) ; this . gutter = elt ( 'div' , null , 'gutter' , `
      position: absolute; left: 0; top: 0;
      min-height: 100%;
      z-index: 3;
      background: #242424;
      text-align: right;
      /* padding: ${ this . gutterPadding [ 1 ] }px ${ this . gutterPadding [ 0 ] }px; */
    ` ) ; this . mover = elt ( 'div' , [ this . lines ] , 'mover' , 'position: relative' ) ; this . sizer = elt ( 'div' , [ this . mover ] , 'sizer' , `
      position: relative;
      box-sizing: content-box;
      border-right: 30px solid transparent;
    ` ) ; this . scroller = controller . wrapper . appendChild ( elt ( 'div' , [ this . gutter , this . sizer ] , 'scroller' , `
      box-sizing: content-box;
      overflow: scroll !important;
      /* 30px is the magic margin used to hide the element's real scrollbars */
      /* Needs overflow: hidden on the wrapper */
      margin-bottom: -30px; margin-right: -30px;
      padding-bottom: 30px;
      position: relative;
    ` ) ) ; on ( this . scroller , 'scroll' , this . onScroll , { capture : false , passive : true } ) ; this . lineCache . on ( 'update' , ( ) => this . updateViewport ( ) ) ; } /**
     * Private API
     */ private measureGutter ( ) { if ( this . drawGutter ) { const asciiWidth = this . metrics . asciiWidth ( ) ; this . gutterChars = nDigits ( this . lineCache . height ( ) ) ; this . gutterWidth = this . gutterChars * asciiWidth + this . gutterPadding [ 0 ] ; } else { this . gutterWidth = 0 ; } this . gutter . style . width = `${ this . gutterWidth }px` ; } private updateViewport ( ) { this . measureGutter ( ) ; this . lines . style . marginBottom = `${ this . scroller . clientHeight }px` ; this . sizer . style . height = `${ this . calculateTotalHeight ( ) }px` ; } private calculateTotalHeight ( ) { return this . metrics . lineHeight ( ) * this . lineCache . height ( ) ; } private onScroll = ( _event ) : void => { if ( this . scroller . clientHeight ) { this . controller . updateViewport ( ) ; off ( this . scroller , 'scroll' , this . onScroll , { capture : false , passive : true } ) ; this . render ( ) ; setTimeout ( ( ) => { on ( this . scroller , 'scroll' , this . onScroll , { capture : false , passive : true } ) ; } ) ; } } ; /**
     * View API
     */ public resize ( width , height ) { this . scroller . style . width = `${ width }px` ; this . scroller . style . height = `${ height }px` ; this . render ( ) ; } public scrollTo ( line , char ) { this . sizer . scrollTop = this . metrics . lineHeight ( ) * line ; this . sizer . scrollLeft = this . metrics . asciiWidth ( ) * char ; } public posFromCoords ( _x , _y ) { return [ 0 , 0 ] ; } public getViewport ( ) { const asciiWidth = this . metrics . asciiWidth ( ) ; const lineHeight = this . metrics . lineHeight ( ) ; const xOffset = 0 ; const { scrollTop , scrollLeft , clientHeight , clientWidth } = this . scroller ; return { lineStart : Math . floor ( scrollTop / lineHeight ) , lineEnd : Math . floor ( ( scrollTop + clientHeight ) / lineHeight ) , charStart : Math . floor ( scrollLeft / asciiWidth ) , charEnd : Math . floor ( ( clientWidth + scrollLeft - xOffset ) / asciiWidth ) , } ; } public render ( ) { removeChildren ( this . lines ) ; removeChildren ( this . gutter ) ; let { lineStart , lineEnd } = this . getViewport ( ) ; this . lineCache . computeMissing ( lineStart , lineEnd ) ; const height = `${ lineStart * this . metrics . lineHeight ( ) }px` ; this . mover . style . top = this . gutter . style . top = height ; this . mover . style . left = `${ this . gutterWidth }px` ; for ( let i = lineStart ; i < lineEnd + 1 ; ++ i ) { const line = this . lineCache . get ( i ) ; if ( ! line ) { continue ; } this . gutter . appendChild ( elt ( 'div' , `${ i }` , null , `
        color: #5a5a5a;
        white-space: normal;
        height: 100%;
        font: ${ this . metrics . fontString ( ) };
      ` ) ) ; const lineNode = new LineNode ( line , this . metrics ) ; this . lines . appendChild ( lineNode . element ) ; } } }