import Vue , { ComponentOptions , CreateElement } from 'vue' ; import { PropsDefinition } from 'vue/types/options' ; import { CombinedVueInstance } from 'vue/types/vue' ; import { VNode } from 'vue/types/vnode' ; import { createNamespace } from '../../utils' ; declare const h ; const [ createComponent , bem ] = createNamespace ( 'tree' ) ; const triangularIcon = `<svg class=${ bem ( 'triangular-icon' ) } width="14" height="14" viewBox="0 0 14 14">
<path d="M5.54297 3.5L9.04297 7L5.54297 10.5L5.54297 3.5Z" stroke-width="1.16667" stroke-linejoin="round"/>
</svg>
` ; const checkIcon = `<svg class=${ bem ( 'check-icon' ) } width="12" height="12" viewBox="0 0 12 12">
<path d="M2.50195 5.99988L5.00195 8.49988L10.002 3.49988" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

` ; const checkPartiallyIcon = `<svg class=${ bem ( 'check-partially-icon' ) } width="12" height="12" viewBox="0 0 12 12">
<path d="M1.5 6H10.5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
` ; export interface TreeValue { /**
     * The value of the node.
     */ value : string | number ; /**
     * Status, 'all' means all of children are selected or the node itself is selected.
     */ status : 'all' | 'partial' ; /**
     * The child nodes.
     */ children ? : TreeValue [ ] ; } export interface TreeOption { /**
     * The id attribute for HTML element.
     */ id ? : string ; /**
     * The text content for display.
     */ label : string ; /**
     * The value of the option.
     */ value ? : string | number ; /**
     * The nested child options, if provided in flat mode, the value of the option itself will be ignored.
     */ children ? : TreeOption [ ] ; } internal export interface TreeOptionSelectedMap { [ p ] : boolean | TreeOptionSelectedMap ; [ p ] : boolean | TreeOptionSelectedMap ; } export interface TreeProps { /**
     * Selected values, use for v-model two-way binding with event `change`
     */ value : ( string | number ) [ ] | TreeValue [ ] ; /**
     * The options array for selecting
     */ options : TreeOption [ ] ; /**
     * Change the behavior of partially selected options to 'unselect all'.
     */ partialToUnselectAll : boolean ; /**
     * Flat mode, use on-dimensional array for bound value.
     * Note that the value of none-end nodes in options will be ignore.
     */ flat : boolean ; } export interface TreeEvents { onChange ( value ) : void ; } export interface TreeData { expandedOptions : TreeOption [ ] ; selectedMap : TreeOptionSelectedMap ; } export interface TreeMethods { switchExpand ( option ) : void ; change ( option , valuePath ) : void ; renderOption ( option , index , level , indent , valuePath ) : VNode ; } export interface TreeComputed { } export type TreeInstance = CombinedVueInstance < Vue , TreeData , TreeMethods , TreeComputed , TreeProps > ; function findValue ( option , values ) { for ( const curValue of values ) { if ( option . value === curValue . value ) { return curValue ; } if ( curValue . children && curValue . children . length > 0 ) { const childResult = findValue ( option , curValue . children ) ; if ( childResult ) { return childResult ; } } } return undefined ; } function mapTreeOptionAllSelected ( options ) { const map = { } ; options . forEach ( option => { const { value , children } = option ; if ( ! children || children . length === 0 ) { map [ value ! ] = true ; return ; } map [ value ! ] = mapTreeOptionAllSelected ( children ) ; } ) ; return map ; } function mapTreeOptions ( options , values ) { const map = { } ; options . forEach ( option => { const value = findValue ( option , values ) ; if ( ! option . children || option . children . length === 0 ) { map [ option . value ! ] = ! ! value ; return ; } map [ option . value ! ] = value ?. status === 'all' ? mapTreeOptionAllSelected ( option . children ) : mapTreeOptions ( option . children , values ) ; } ) ; return map ; } function formTreeValuesFromMap ( options , map ) { return options . map < TreeValue | undefined > ( option => { const { value , children : childOptions } = option ; const subMap = map [ value ! ] ; if ( ! childOptions || childOptions . length === 0 ) { if ( subMap ) { return { value : value ! , status : 'all' , } ; } return undefined ; } const children = formTreeValuesFromMap ( childOptions , subMap as TreeOptionSelectedMap ) ; if ( children . length === 0 ) { return undefined ; } const status = children . length < childOptions . length || children . some ( c => c . status === 'partial' ) ? 'partial' : 'all' ; return { value : value ! , children , status , } ; } ) . filter ( ( v ) : v is TreeValue => ! ! v ) ; } function getSubMap ( map , valuePath ) { let subMap = map ; valuePath . forEach ( segment => { if ( typeof subMap === 'object' ) { subMap = subMap [ segment ] ; } } ) ; return subMap ; } function modifyMap ( map , target ) { Object . entries ( map ) . forEach ( ( [ value , subMap ] ) => { if ( typeof subMap === 'boolean' ) { map [ value ] = target ; } else { modifyMap ( subMap , target ) ; } } ) ; } type TreeSelectedStatus = 'all' | 'partial' | 'none' ; function getOptionStatus ( map , option , valuePath ) { const { children } = option ; const subMap = getSubMap ( map , valuePath ) ; if ( ! children || children . length === 0 ) { if ( subMap ) { return 'all' ; } else { return 'none' ; } } const { length } = children ; let amountAll = 0 ; let amountNone = 0 ; children . forEach ( c => { switch ( getOptionStatus ( map , c , [ ... valuePath , c . value ! ] ) ) { case 'all' : amountAll += 1 ; break ; case 'none' : amountNone += 1 ; break ; default : break ; } } ) ; if ( amountAll === length ) { return 'all' ; } if ( amountNone === length ) { return 'none' ; } return 'partial' ; } function getOptionStatusFlat ( selectedValues , option ) { const { value , children } = option ; if ( ! children || children . length === 0 ) { if ( selectedValues . includes ( value ! ) ) { return 'all' ; } else { return 'none' ; } } const { length } = children ; let amountAll = 0 ; let amountNone = 0 ; children . forEach ( c => { switch ( getOptionStatusFlat ( selectedValues , c ) ) { case 'all' : amountAll += 1 ; break ; case 'none' : amountNone += 1 ; break ; default : break ; } } ) ; if ( amountAll === length ) { return 'all' ; } if ( amountNone === length ) { return 'none' ; } return 'partial' ; } function getOptionValuesFlat ( option ) { const { value , children } = option ; if ( ! children || children . length === 0 ) { return [ value ! ] ; } return children . map ( c => getOptionValuesFlat ( c ) ) . flat ( 1 ) ; } const Tree = { data ( ) { return { expandedOptions : [ ] , selectedMap : { } , } ; } , model : { prop : 'value' , event : 'change' , } , props : { value : { type : Array , required : true , } , options : { type : Array , required : true , } , partialToUnselectAll : { type : Boolean , default : false , } , flat : { type : Boolean , default : false , } , } , watch : { value : { immediate : true , handler ( this , val , oldVal ) { if ( ! this . flat ) { if ( val !== oldVal ) { this . selectedMap = mapTreeOptions ( this . options , val as TreeValue [ ] ) ; } } } , } , options : { handler ( this , _options ) { this . expandedOptions = [ ] ; } , } , } , methods : { switchExpand ( this , option ) { if ( this . expandedOptions . includes ( option ) ) { this . expandedOptions = this . expandedOptions . filter ( o => o !== option ) ; return ; } this . expandedOptions . push ( option ) ; } , change ( this , option , valuePath ) { const { value : selectedValues , options , partialToUnselectAll , flat , selectedMap } = this ; if ( flat ) { const selectedValuesFlat = selectedValues as ( string | number ) [ ] ; const optionStatus = getOptionStatusFlat ( selectedValuesFlat , option ) ; const optionValues = getOptionValuesFlat ( option ) ; const newValue = optionStatus === 'none' || ( optionStatus === 'partial' && ! partialToUnselectAll ) ? Array . from ( new Set ( [ ... selectedValuesFlat , ... optionValues ] ) ) : selectedValuesFlat . filter ( v => ! optionValues . includes ( v ) ) ; this . $emit ( 'change' , newValue ) ; } else { const optionStatus = getOptionStatus ( selectedMap , option , valuePath ) ; const subMap = getSubMap ( selectedMap , valuePath ) ; const target = optionStatus === 'none' || ( optionStatus === 'partial' && ! partialToUnselectAll ) ; if ( typeof subMap === 'object' ) { modifyMap ( subMap , target ) ; } else { const parentMap = getSubMap ( selectedMap , valuePath . slice ( 0 , valuePath . length - 1 ) ) as TreeOptionSelectedMap ; parentMap [ valuePath [ valuePath . length - 1 ] ] = target ; } const newValue = formTreeValuesFromMap ( options , selectedMap ) ; this . $emit ( 'change' , newValue ) ; } } , renderOption ( this , option , _index , level , indent , valuePath ) { const { value : selectedValues , flat , expandedOptions , selectedMap } = this ; const { id , label , value , children } = option ; const hasChildren = ! ! children && children . length > 0 ; const hasGrandchildren = hasChildren && children ! . some ( c => c . children && c . children . length > 0 ) ; const expanded = expandedOptions . includes ( option ) ; const currentValuePath = [ ... valuePath , value ! ] ; const status = flat ? getOptionStatusFlat ( selectedValues as ( string | number ) [ ] , option ) : getOptionStatus ( selectedMap , option , currentValuePath ) ; const onExpand = ( event ) => { event . stopPropagation ( ) ; event . preventDefault ( ) ; this . switchExpand ( option ) ; } ; const onChange = ( event ) => { event . stopPropagation ( ) ; event . preventDefault ( ) ; this . change ( option , currentValuePath ) ; } ; return ( < div class = { bem ( 'node' ) } > < div class = { bem ( 'container' ) } id = { id } onClick = { ( hasChildren && onExpand ) || onChange } > { ( hasChildren && ( < i class = { bem ( 'triangular' , { expanded } ) } domPropsInnerHTML = { triangularIcon } > < /i>)) || ( indent && < i class = { bem ( 'triangular' ) } > < /i>)} < span class = { bem ( 'label' ) } > { label } < /span> < i class = { bem ( 'check' , `status-${ status } `)} domPropsInnerHTML={(status === 'all' && checkIcon) ||
                    (status === 'partial' && checkPartiallyIcon) ||
                    ''} onClick={onChange}></i>
          </div>

          <div class={bem('children', { expanded })}>
            {hasChildren &&
                    children!.map((c, i) => this.renderOption(c, i, level + 1, hasGrandchildren, currentValuePath))}
          </div>
        </div>);
        },
    },
    render(this) {
        const { options } = this;
        const hasChildren = options.some(o => o.children && o.children.length > 0);
        const currentValuePath = [];
        const classes = bem();
        return (<div class={classes}>
        {options.map((option, index) => this.renderOption(option, index, 0, hasChildren, currentValuePath))}
      </div>);
    },
};
export default createComponent(Tree as any);
