import { getType , isObject , getObjectName , getValuePreview , getPreview , cssClass , createElement } from './helpers' ; import './style.less' ; const DATE_STRING_REGEX = /(^\d{1,4}[\.|\\/|-]\d{1,2}[\.|\\/|-]\d{1,4})(\s*(?:0?[1-9]:[0-5]|1(?=[012])\d:[0-5])\d\s*[ap]m)?$/ ; const PARTIAL_DATE_REGEX = /\d{2}:\d{2}:\d{2} GMT-\d{4}/ ; const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ ; const MAX_ANIMATED_TOGGLE_ITEMS = 10 ; const requestAnimationFrame = window . requestAnimationFrame || function ( cb ) { cb ( ) ; return 0 ; } ; export interface JSONFormatterConfiguration { hoverPreviewEnabled ? : boolean ; hoverPreviewArrayCount ? : number ; hoverPreviewFieldCount ? : number ; animateOpen ? : boolean ; animateClose ? : boolean ; theme ? : string ; useToJSON ? : boolean ; sortPropertiesBy ? : ( a : string , b : string ) => number ; } ; const _defaultConfig = { hoverPreviewEnabled : false , hoverPreviewArrayCount : 100 , hoverPreviewFieldCount : 5 , animateOpen : true , animateClose : true , theme : null , useToJSON : true , sortPropertiesBy : null } ; class export default class JSONFormatter { private _isOpen : boolean = null ; private element : Element ; param { object } json param { number } open param { object } config param { string } key constructor ( public json , private open = 1 , private config = _defaultConfig , private key ? ) { if ( this . config . hoverPreviewEnabled === undefined ) { this . config . hoverPreviewEnabled = _defaultConfig . hoverPreviewEnabled ; } if ( this . config . hoverPreviewArrayCount === undefined ) { this . config . hoverPreviewArrayCount = _defaultConfig . hoverPreviewArrayCount ; } if ( this . config . hoverPreviewFieldCount === undefined ) { this . config . hoverPreviewFieldCount = _defaultConfig . hoverPreviewFieldCount ; } if ( this . config . useToJSON === undefined ) { this . config . useToJSON = _defaultConfig . useToJSON ; } if ( this . key === '' ) { this . key = '""' ; } } private get isOpen ( ) : boolean { if ( this . _isOpen !== null ) { return this . _isOpen ; } else { return this . open > 0 ; } } private set isOpen ( value ) { this . _isOpen = value ; } private get isDate ( ) : boolean { return ( ( this . json instanceof Date ) || ( ( this . type === 'string' ) && ( DATE_STRING_REGEX . test ( this . json ) || JSON_DATE_REGEX . test ( this . json ) || PARTIAL_DATE_REGEX . test ( this . json ) ) ) ) ; } private get isUrl ( ) : boolean { return this . type === 'string' && ( this . json . indexOf ( 'http' ) === 0 ) ; } private get isArray ( ) : boolean { return Array . isArray ( this . json ) ; } private get isObject ( ) : boolean { return isObject ( this . json ) ; } private get isEmptyObject ( ) : boolean { return ! this . keys . length && ! this . isArray ; } private get isEmpty ( ) : boolean { return this . isEmptyObject || ( this . keys && ! this . keys . length && this . isArray ) ; } private get useToJSON ( ) : boolean { return this . config . useToJSON && this . type === 'stringifiable' ; } private get hasKey ( ) : boolean { return typeof this . key !== 'undefined' ; } private get constructorName ( ) : string { return getObjectName ( this . json ) ; } private get type ( ) : string { if ( this . config . useToJSON && this . json && this . json [ 'toJSON' ] ) { return 'stringifiable' ; } return getType ( this . json ) ; } private get keys ( ) : string [ ] { if ( this . isObject ) { const keys = Object . keys ( this . json ) ; return ( ! this . isArray && this . config . sortPropertiesBy ) ? keys . sort ( this . config . sortPropertiesBy ) : keys ; } else { return [ ] ; } } /**
     * Toggles `isOpen` state
     *
    */ toggleOpen ( ) { this . isOpen = ! this . isOpen ; if ( this . element ) { if ( this . isOpen ) { this . appendChildren ( this . config . animateOpen ) ; } else { this . removeChildren ( this . config . animateClose ) ; } this . element . classList . toggle ( cssClass ( 'open' ) ) ; } } /**
    * Open all children up to a certain depth.
    * Allows actions such as expand all/collapse all
    *
    */ openAtDepth ( depth = 1 ) { if ( depth < 0 ) { return ; } this . open = depth ; this . isOpen = ( depth !== 0 ) ; if ( this . element ) { this . removeChildren ( false ) ; if ( depth === 0 ) { this . element . classList . remove ( cssClass ( 'open' ) ) ; } else { this . appendChildren ( this . config . animateOpen ) ; this . element . classList . add ( cssClass ( 'open' ) ) ; } } } returns { string } getInlinepreview ( ) { if ( this . isArray ) { if ( this . json . length > this . config . hoverPreviewArrayCount ) { return `Array[${ this . json . length }]` ; } else { return `[${ this . json . map ( getPreview ) . join ( ', ' ) }]` ; } } else { const keys = this . keys ; const narrowKeys = keys . slice ( 0 , this . config . hoverPreviewFieldCount ) ; const kvs = narrowKeys . map ( key => `${ key }:${ getPreview ( this . json [ key ] ) }` ) ; const ellipsis = keys . length >= this . config . hoverPreviewFieldCount ? '…' : '' ; return `{${ kvs . join ( ', ' ) }${ ellipsis }}` ; } } returns { HTMLDivElement } render ( ) { this . element = createElement ( 'div' , 'row' ) ; const togglerLink = this . isObject ? createElement ( 'a' , 'toggler-link' ) : createElement ( 'span' ) ; if ( this . isObject && ! this . useToJSON ) { togglerLink . appendChild ( createElement ( 'span' , 'toggler' ) ) ; } if ( this . hasKey ) { togglerLink . appendChild ( createElement ( 'span' , 'key' , `${ this . key }:` ) ) ; } if ( this . isObject && ! this . useToJSON ) { const value = createElement ( 'span' , 'value' ) ; const objectWrapperSpan = createElement ( 'span' ) ; var constructorName = createElement ( 'span' , 'constructor-name' , this . constructorName ) ; objectWrapperSpan . appendChild ( constructorName ) ; if ( this . isArray ) { const arrayWrapperSpan = createElement ( 'span' ) ; arrayWrapperSpan . appendChild ( createElement ( 'span' , 'bracket' , '[' ) ) ; arrayWrapperSpan . appendChild ( createElement ( 'span' , 'number' , ( this . json . length ) ) ) ; arrayWrapperSpan . appendChild ( createElement ( 'span' , 'bracket' , ']' ) ) ; objectWrapperSpan . appendChild ( arrayWrapperSpan ) ; } value . appendChild ( objectWrapperSpan ) ; togglerLink . appendChild ( value ) ; } else { const value = this . isUrl ? createElement ( 'a' ) : createElement ( 'span' ) ; value . classList . add ( cssClass ( this . type ) ) ; if ( this . isDate ) { value . classList . add ( cssClass ( 'date' ) ) ; } if ( this . isUrl ) { value . classList . add ( cssClass ( 'url' ) ) ; value . setAttribute ( 'href' , this . json ) ; } const valuePreview = getValuePreview ( this . type , this . json , this . useToJSON ? this . json . toJSON ( ) : this . json ) ; value . appendChild ( document . createTextNode ( valuePreview ) ) ; togglerLink . appendChild ( value ) ; } if ( this . isObject && this . config . hoverPreviewEnabled ) { const preview = createElement ( 'span' , 'preview-text' ) ; preview . appendChild ( document . createTextNode ( this . getInlinepreview ( ) ) ) ; togglerLink . appendChild ( preview ) ; } const children = createElement ( 'div' , 'children' ) ; if ( this . isObject ) { children . classList . add ( cssClass ( 'object' ) ) ; } if ( this . isArray ) { children . classList . add ( cssClass ( 'array' ) ) ; } if ( this . isEmpty ) { children . classList . add ( cssClass ( 'empty' ) ) ; } if ( this . config && this . config . theme ) { this . element . classList . add ( cssClass ( this . config . theme ) ) ; } if ( this . isOpen ) { this . element . classList . add ( cssClass ( 'open' ) ) ; } this . element . appendChild ( togglerLink ) ; this . element . appendChild ( children ) ; if ( this . isObject && this . isOpen ) { this . appendChildren ( ) ; } if ( this . isObject && ! this . useToJSON ) { togglerLink . addEventListener ( 'click' , this . toggleOpen . bind ( this ) ) ; } return this . element as HTMLDivElement ; } /**
     * Appends all the children to children element
     * Animated option is used when user triggers this via a click
    */ appendChildren ( animated = false ) { const children = this . element . querySelector ( `div.${ cssClass ( 'children' ) }` ) ; if ( ! children || this . isEmpty ) { return ; } if ( animated ) { let index = 0 ; const addAChild = ( ) => { const key = this . keys [ index ] ; const formatter = new JSONFormatter ( this . json [ key ] , this . open - 1 , this . config , key ) ; children . appendChild ( formatter . render ( ) ) ; index += 1 ; if ( index < this . keys . length ) { if ( index > MAX_ANIMATED_TOGGLE_ITEMS ) { addAChild ( ) ; } else { requestAnimationFrame ( addAChild ) ; } } } ; requestAnimationFrame ( addAChild ) ; } else { this . keys . forEach ( key => { const formatter = new JSONFormatter ( this . json [ key ] , this . open - 1 , this . config , key ) ; children . appendChild ( formatter . render ( ) ) ; } ) ; } } /**
     * Removes all the children from children element
     * Animated option is used when user triggers this via a click
    */ removeChildren ( animated = false ) { const childrenElement = this . element . querySelector ( `div.${ cssClass ( 'children' ) }` ) as HTMLDivElement ; if ( animated ) { let childrenRemoved = 0 ; const removeAChild = ( ) => { if ( childrenElement && childrenElement . children . length ) { childrenElement . removeChild ( childrenElement . children [ 0 ] ) ; childrenRemoved += 1 ; if ( childrenRemoved > MAX_ANIMATED_TOGGLE_ITEMS ) { removeAChild ( ) ; } else { requestAnimationFrame ( removeAChild ) ; } } } ; requestAnimationFrame ( removeAChild ) ; } else { if ( childrenElement ) { childrenElement . innerHTML = '' ; } } } }