/**
 * -------------------------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation.  All Rights Reserved.  Licensed under the MIT License.
 * See License in the project root for license information.
 * -------------------------------------------------------------------------------------------
 */ import * as MicrosoftGraph from '@microsoft/microsoft-graph-types' ; import { customElement , html , property , TemplateResult } from 'lit-element' ; import { classMap } from 'lit-html/directives/class-map' ; import { Providers , ProviderState , MgtTemplatedComponent } from '@microsoft/mgt-element' ; import '../../styles/style-helper' ; import '../sub-components/mgt-spinner/mgt-spinner' ; import { getSvg , SvgIcon } from '../../utils/SvgHelper' ; import { debounce } from '../../utils/Utils' ; import { styles } from './mgt-teams-channel-picker-css' ; import { getAllMyTeams } from './mgt-teams-channel-picker.graph' ; import { strings } from './strings' ; export interface export type Team = MicrosoftGraph . Team & { type { string } displayName ? : string ; } ; export interface export interface SelectedChannel { type { MicrosoftGraph . Channel } memberof channel : MicrosoftGraph . Channel ; type { MicrosoftGraph . Team } memberof team : Team ; } export interface interface DropdownItem { type { DropdownItem [ ] } memberof channels ? : DropdownItem [ ] ; type { ( MicrosoftGraph . Channel | MicrosoftGraph . Team ) } memberof item : MicrosoftGraph . Channel | Team ; } interface interface ChannelPickerItemState { type { ( MicrosoftGraph . Channel | MicrosoftGraph . Team ) } memberof item : MicrosoftGraph . Channel | Team ; type { boolean } memberof isExpanded ? : boolean ; type { ChannelPickerItemState [ ] } memberof channels ? : ChannelPickerItemState [ ] ; type { ChannelPickerItemState } memberof parent : ChannelPickerItemState ; } export interface export interface MgtTeamsChannelPickerConfig { type { boolean } useTeamsBasedScopes : boolean ; } class extends MgtTemplatedComponent fires cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop @ customElement ( 'mgt-teams-channel-picker' ) export class MgtTeamsChannelPicker extends MgtTemplatedComponent { /**
     * Array of styles to apply to the element. The styles should be defined
     * user the `css` tag function.
     */ static get styles ( ) { return styles ; } protected get strings ( ) { return strings ; } static type { MgtTeamsChannelPickerConfig } memberof public static get config ( ) : MgtTeamsChannelPickerConfig { return this . _config ; } private static _config = { useTeamsBasedScopes : false } ; readonly type { SelectedChannel } memberof public get selectedItem ( ) : SelectedChannel { if ( this . _selectedItemState ) { return { channel : this . _selectedItemState . item , team : this . _selectedItemState . parent . item } ; } else { return null ; } } static return { * } memberof public static get requiredScopes ( ) : string [ ] { if ( this . config . useTeamsBasedScopes ) { return [ 'team.readbasic.all' , 'channel.readbasic.all' ] ; } else { return [ 'user.read.all' , 'group.read.all' ] ; } } private set items ( value ) { if ( this . _items === value ) { return ; } this . _items = value ; this . _treeViewState = value ? this . generateTreeViewState ( value ) : [ ] ; this . resetFocusState ( ) ; } private get items ( ) : DropdownItem [ ] { return this . _items ; } private get _input ( ) : HTMLElement { return this . renderRoot . querySelector ( '.team-chosen-input' ) ; } private _inputValue : string = '' ; private _isFocused = false ; private _selectedItemState : ChannelPickerItemState ; private _items : DropdownItem [ ] ; private _treeViewState : ChannelPickerItemState [ ] = [ ] ; private _focusList : ChannelPickerItemState [ ] = [ ] ; private _focusedIndex : number = - 1 ; private debouncedSearch ; @ property ( { attribute : false } ) private _isDropdownVisible ; constructor ( ) { super ( ) ; this . handleWindowClick = this . handleWindowClick . bind ( this ) ; this . addEventListener ( 'keydown' , e => this . onUserKeyDown ( e ) ) ; this . addEventListener ( 'focus' , _ => this . loadTeamsIfNotLoaded ( ) ) ; this . addEventListener ( 'mouseover' , _ => this . loadTeamsIfNotLoaded ( ) ) ; } memberof public connectedCallback ( ) { super . connectedCallback ( ) ; window . addEventListener ( 'click' , this . handleWindowClick ) ; } memberof public disconnectedCallback ( ) { window . removeEventListener ( 'click' , this . handleWindowClick ) ; super . disconnectedCallback ( ) ; } param { string } channelId returns { Promise < memberof public async selectChannelById ( channelId ) { const provider = Providers . globalProvider ; if ( provider && provider . state === ProviderState . SignedIn ) { if ( ! this . items ) { await this . requestStateUpdate ( ) ; } for ( const item of this . _treeViewState ) { for ( const channel of item . channels ) { if ( channel . item . id === channelId ) { this . selectChannel ( channel ) ; return true ; } } } } return false ; } returns memberof public render ( ) { const inputClasses = { focused : this . _isFocused , 'input-wrapper' : true } ; const iconClasses = { focused : this . _isFocused && ! ! this . _selectedItemState , 'search-icon' : true } ; const dropdownClasses = { dropdown : true , visible : this . _isDropdownVisible } ; const searchClasses = { 'hide-icon' : ! ! this . _selectedItemState , 'search-wrapper' : true } ; return ( this . renderTemplate ( 'default' , { teams : this . items } ) || html `
        <div class="root" @blur=${ this . lostFocus } dir=${ this . direction }>
          <div class=${ classMap ( inputClasses ) } @click=${ this . gainedFocus }>
            ${ this . renderSelected ( ) }
            <div class=${ classMap ( searchClasses ) }>${ this . renderSearchIcon ( ) } ${ this . renderInput ( ) }</div>
          </div>
          ${ this . renderCloseButton ( ) }
          <div class=${ classMap ( dropdownClasses ) }>${ this . renderDropdown ( ) }</div>
        </div>
      ` ) ; } protected returns memberof protected renderSelected ( ) { if ( ! this . _selectedItemState ) { return html `` ; } return html `
      <li class="selected-team" title=${ this . _selectedItemState . item . displayName }>
        <div class="selected-team-name">${ this . _selectedItemState . parent . item . displayName }</div>
        <div class="arrow">${ getSvg ( SvgIcon . TeamSeparator , '#B3B0AD' ) }</div>
        ${ this . _selectedItemState . item . displayName }
        <div class="search-wrapper">${ this . renderSearchIcon ( ) } ${ this . renderInput ( ) }</div>
      </li>
    ` ; } protected memberof protected clearState ( ) { this . _items = [ ] ; this . _inputValue = '' ; this . _treeViewState = [ ] ; this . _focusList = [ ] ; } protected returns memberof protected renderSearchIcon ( ) { return html `
      <div class="search-icon">
        ${ getSvg ( SvgIcon . Search , '#252424' ) }
      </div>
    ` ; } protected returns memberof protected renderInput ( ) { const rootClasses = { 'input-search' : ! ! this . _selectedItemState , 'input-search-start' : ! this . _selectedItemState } ; return html `
      <div class="${ classMap ( rootClasses ) }">
        <span
          id="teams-channel-picker-input"
          class="team-chosen-input"
          type="text"
          label="teams-channel-picker-input"
          aria-label="Select a channel"
          data-placeholder="${ ! ! this . _selectedItemState ? '' : this . strings . inputPlaceholderText } "
          role="input"
          @keyup=${ e => this . handleInputChanged ( e ) }
          contenteditable
        ></span>
      </div>
    ` ; } protected returns memberof protected renderCloseButton ( ) { return html `
      <div class="close-icon" @click="${ ( ) => this . selectChannel ( null ) }">
        
      </div>
    ` ; } param { ChannelPickerItemState [ ] } items param { number } level returns memberof protected renderDropdown ( ) { if ( this . isLoadingState || ! this . _treeViewState ) { return this . renderLoading ( ) ; } if ( this . _treeViewState ) { if ( ! this . isLoadingState && this . _treeViewState . length === 0 && this . _inputValue . length > 0 ) { return this . renderError ( ) ; } return this . renderDropdownList ( this . _treeViewState ) ; } return html `` ; } protected param { ChannelPickerItemState [ ] } items param { number } level returns memberof protected renderDropdownList ( items , level = 0 ) { if ( items && items . length ) { return items . map ( ( treeItem , index ) => { const isLeaf = ! treeItem . channels ; const renderChannels = ! isLeaf && treeItem . isExpanded ; return html `
          ${ this . renderItem ( treeItem ) }
          ${ renderChannels ? this . renderDropdownList ( treeItem . channels , level + 1 ) : html `` }
        ` ; } ) ; } return null ; } param { ChannelPickerItemState } itemState returns memberof protected renderItem ( itemState ) { let icon = null ; if ( itemState . channels ) { icon = itemState . isExpanded ? getSvg ( SvgIcon . ArrowDown , '#252424' ) : getSvg ( SvgIcon . ArrowRight , '#252424' ) ; } let isSelected = false ; if ( this . selectedItem ) { if ( this . selectedItem . channel === itemState . item ) { isSelected = true ; } } const classes = { focused : this . _focusList [ this . _focusedIndex ] === itemState , item : true , 'list-team' : itemState . channels ? true : false , selected : isSelected } ; const dropDown = this . renderRoot . querySelector ( '.dropdown' ) ; if ( dropDown . children [ this . _focusedIndex ] ) { dropDown . children [ this . _focusedIndex ] . scrollIntoView ( false ) ; } return html `
      <div @click=${ ( ) => this . handleItemClick ( itemState ) } class="${ classMap ( classes ) }">
        <div class="arrow">
          ${ icon }
        </div>
        ${ itemState . channels ? itemState . item . displayName : this . renderHighlightedText ( itemState . item ) }
      </div>
    ` ; } protected param { * } channel returns memberof protected renderHighlightedText ( channel ) { let channels = { } ; const highlightLocation = channel . displayName . toLowerCase ( ) . indexOf ( this . _inputValue . toLowerCase ( ) ) ; if ( highlightLocation !== - 1 ) { if ( highlightLocation === 0 ) { channels . first = '' ; channels . highlight = channel . displayName . slice ( 0 , this . _inputValue . length ) ; channels . last = channel . displayName . slice ( this . _inputValue . length , channel . displayName . length ) ; } else if ( highlightLocation === channel . displayName . length ) { channels . first = channel . displayName . slice ( 0 , highlightLocation ) ; channels . highlight = channel . displayName . slice ( highlightLocation , channel . displayName . length ) ; channels . last = '' ; } else { channels . first = channel . displayName . slice ( 0 , highlightLocation ) ; channels . highlight = channel . displayName . slice ( highlightLocation , highlightLocation + this . _inputValue . length ) ; channels . last = channel . displayName . slice ( highlightLocation + this . _inputValue . length , channel . displayName . length ) ; } } else { channels . last = channel . displayName ; } return html `
      <div class="channel-display">
        <div class="showing">
          <span class="channel-name-text">${ channels . first }</span
          ><span class="channel-name-text highlight-search-text">${ channels . highlight }</span
          ><span class="channel-name-text">${ channels . last }</span>
        </div>
      </div>
    ` ; } protected returns memberof protected renderError ( ) { const template = this . renderTemplate ( 'error' , null , 'error' ) ; return ( template || html `
        <div class="message-parent">
          <div label="search-error-text" aria-label="We didn't find any matches." class="search-error-text">
            ${ this . strings . noResultsFound }
          </div>
        </div>
      ` ) ; } protected returns memberof protected renderLoading ( ) { const template = this . renderTemplate ( 'loading' , null , 'loading' ) ; return ( template || html `
        <div class="message-parent">
          <mgt-spinner></mgt-spinner>
          <div label="loading-text" aria-label="loading" class="loading-text">
            ${ this . strings . loadingMessage }
          </div>
        </div>
      ` ) ; } protected memberof protected async loadState ( ) { const provider = Providers . globalProvider ; let teams ; if ( provider && provider . state === ProviderState . SignedIn ) { const graph = provider . graph . forComponent ( this ) ; if ( ! ( await provider . getAccessTokenForScopes ( ... MgtTeamsChannelPicker . requiredScopes ) ) ) { return ; } teams = await getAllMyTeams ( graph ) ; teams = teams . filter ( t => ! t . isArchived ) ; const batch = graph . createBatch ( ) ; const scopes = [ 'team.readbasic.all' ] ; for ( const team of teams ) { batch . get ( team . id , `teams/${ team . id }/channels` , scopes ) ; } const responses = await batch . executeAll ( ) ; for ( const team of teams ) { const response = responses . get ( team . id ) ; if ( response && response . content && response . content . value ) { team . channels = response . content . value . map ( c => { return { item : c } ; } ) ; } } this . items = teams . map ( t => { return { channels : t . channels as DropdownItem [ ] , item : t } ; } ) ; } this . filterList ( ) ; this . resetFocusState ( ) ; } private handleItemClick ( item ) { if ( item . channels ) { item . isExpanded = ! item . isExpanded ; } else { this . selectChannel ( item ) ; } this . _focusedIndex = - 1 ; this . resetFocusState ( ) ; } private handleInputChanged ( e ) { if ( this . _inputValue !== e . target . textContent ) { this . _inputValue = e . target . textContent ; } else { return ; } this . gainedFocus ( ) ; if ( ! this . debouncedSearch ) { this . debouncedSearch = debounce ( ( ) => { this . filterList ( ) ; } , 400 ) ; } this . debouncedSearch ( ) ; } private filterList ( ) { if ( this . items ) { this . _treeViewState = this . generateTreeViewState ( this . items , this . _inputValue ) ; this . _focusedIndex = - 1 ; this . resetFocusState ( ) ; } } private generateTreeViewState ( tree , filterString = '' , parent = null ) { const treeView = [ ] ; filterString = filterString . toLowerCase ( ) ; if ( tree ) { for ( const state of tree ) { let stateItem ; if ( filterString . length === 0 || state . item . displayName . toLowerCase ( ) . includes ( filterString ) ) { stateItem = { item : state . item , parent } ; if ( state . channels ) { stateItem . channels = this . generateTreeViewState ( state . channels , '' , stateItem ) ; stateItem . isExpanded = filterString . length > 0 ; } } else if ( state . channels ) { const newStateItem = { item : state . item , parent } ; const channels = this . generateTreeViewState ( state . channels , filterString , newStateItem ) ; if ( channels . length > 0 ) { stateItem = newStateItem ; stateItem . channels = channels ; stateItem . isExpanded = true ; } } if ( stateItem ) { treeView . push ( stateItem ) ; } } } return treeView ; } private generateFocusList ( items ) { if ( ! items || items . length === 0 ) { return [ ] ; } let array = [ ] ; for ( const item of items ) { array . push ( item ) ; if ( item . channels && item . isExpanded ) { array = [ ... array , ... this . generateFocusList ( item . channels ) ] ; } } return array ; } private resetFocusState ( ) { this . _focusList = this . generateFocusList ( this . _treeViewState ) ; this . requestUpdate ( ) ; } private loadTeamsIfNotLoaded ( ) { if ( ! this . items && ! this . isLoadingState ) { this . requestStateUpdate ( ) ; } } private handleWindowClick ( e ) { if ( e . target !== this ) { this . lostFocus ( ) ; } } private onUserKeyDown ( event ) { if ( event . keyCode === 13 ) { event . preventDefault ( ) ; } if ( this . _treeViewState . length === 0 ) { return ; } const currentFocusedItem = this . _focusList [ this . _focusedIndex ] ; switch ( event . keyCode ) { case 40 : this . _focusedIndex = ( this . _focusedIndex + 1 ) % this . _focusList . length ; this . requestUpdate ( ) ; event . preventDefault ( ) ; break ; case 38 : if ( this . _focusedIndex === - 1 ) { this . _focusedIndex = this . _focusList . length ; } this . _focusedIndex = ( this . _focusedIndex - 1 + this . _focusList . length ) % this . _focusList . length ; this . requestUpdate ( ) ; event . preventDefault ( ) ; break ; case 39 : if ( currentFocusedItem && currentFocusedItem . channels && ! currentFocusedItem . isExpanded ) { currentFocusedItem . isExpanded = true ; this . resetFocusState ( ) ; event . preventDefault ( ) ; } break ; case 37 : if ( currentFocusedItem && currentFocusedItem . channels && currentFocusedItem . isExpanded ) { currentFocusedItem . isExpanded = false ; this . resetFocusState ( ) ; event . preventDefault ( ) ; } break ; case 9 : if ( ! currentFocusedItem ) { this . lostFocus ( ) ; break ; } case 13 : if ( currentFocusedItem && currentFocusedItem . channels ) { currentFocusedItem . isExpanded = ! currentFocusedItem . isExpanded ; this . resetFocusState ( ) ; event . preventDefault ( ) ; } else if ( currentFocusedItem && ! currentFocusedItem . channels ) { this . selectChannel ( currentFocusedItem ) ; this . resetFocusState ( ) ; this . _focusedIndex = - 1 ; event . preventDefault ( ) ; } break ; case 8 : if ( this . _inputValue . length === 0 && this . _selectedItemState ) { this . selectChannel ( null ) ; event . preventDefault ( ) ; } break ; case 27 : this . selectChannel ( this . _selectedItemState ) ; this . _focusedIndex = - 1 ; this . resetFocusState ( ) ; event . preventDefault ( ) ; break ; } } private gainedFocus ( ) { this . _isFocused = true ; const input = this . _input ; if ( input ) { input . focus ( ) ; } this . _isDropdownVisible = true ; } private lostFocus ( ) { this . _isFocused = false ; const input = this . _input ; if ( input ) { input . textContent = this . _inputValue = '' ; } this . _isDropdownVisible = false ; this . filterList ( ) ; } private selectChannel ( item ) { if ( this . _selectedItemState !== item ) { this . _selectedItemState = item ; this . fireCustomEvent ( 'selectionChanged' , item ? [ this . selectedItem ] : [ ] ) ; } const input = this . _input ; if ( input ) { input . textContent = this . _inputValue = '' ; } this . requestUpdate ( ) ; this . lostFocus ( ) ; } }