/**
 * -------------------------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation.  All Rights Reserved.  Licensed under the MIT License.
 * See License in the project root for license information.
 * -------------------------------------------------------------------------------------------
 */ import { User } from '@microsoft/microsoft-graph-types' ; import { customElement , html , internalProperty , property , TemplateResult } from 'lit-element' ; import { classMap } from 'lit-html/directives/class-map' ; import { repeat } from 'lit-html/directives/repeat' ; import { findGroups , getGroupsForGroupIds , GroupType , getGroup } from '../../graph/graph.groups' ; import { findPeople , getPeople , PersonType , UserType } from '../../graph/graph.people' ; import { findUsers , findGroupMembers , getUser , getUsersForUserIds } from '../../graph/graph.user' ; import { IDynamicPerson , ViewType } from '../../graph/types' ; import { Providers , ProviderState , MgtTemplatedComponent , arraysAreEqual } from '@microsoft/mgt-element' ; import '../../styles/style-helper' ; import '../sub-components/mgt-spinner/mgt-spinner' ; import { debounce , isValidEmail } from '../../utils/Utils' ; import { MgtPerson } from '../mgt-person/mgt-person' ; import { PersonCardInteraction } from '../PersonCardInteraction' ; import { MgtFlyout } from '../sub-components/mgt-flyout/mgt-flyout' ; import { styles } from './mgt-people-picker-css' ; import { strings } from './strings' ; export { GroupType } from '../../graph/graph.groups' ; export { PersonType , UserType } from '../../graph/graph.people' ; interface interface IFocusable { isFocused : boolean ; } export class extends MgtTemplatedComponent fires cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop cssprop @ customElement ( 'mgt-people-picker' ) export class MgtPeoplePicker 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 ; } protected type { MgtFlyout } memberof protected get flyout ( ) : MgtFlyout { return this . renderRoot . querySelector ( '.flyout' ) ; } protected type { MgtFlyout } memberof protected get input ( ) : HTMLInputElement { return this . renderRoot . querySelector ( '.search-box__input' ) ; } type { string } @ property ( { attribute : 'group-id' } ) public get groupId ( ) : string { return this . _groupId ; } public set groupId ( value ) { if ( this . _groupId === value ) { return ; } this . _groupId = value ; this . requestStateUpdate ( true ) ; } type { PersonType } @ property ( { attribute : 'type' , converter : ( value , type ) => { value = value . toLowerCase ( ) ; if ( ! value || value . length === 0 ) { return PersonType . any ; } if ( typeof PersonType [ value ] === 'undefined' ) { return PersonType . any ; } else { return PersonType [ value ] ; } } } ) public get type ( ) : PersonType { return this . _type ; } public set type ( value ) { if ( this . _type === value ) { return ; } this . _type = value ; this . requestStateUpdate ( true ) ; } type { GroupType } @ property ( { attribute : 'group-type' , converter : ( value , type ) => { if ( ! value || value . length === 0 ) { return GroupType . any ; } const values = value . split ( ',' ) ; const groupTypes = [ ] ; for ( let v of values ) { v = v . trim ( ) ; if ( typeof GroupType [ v ] !== 'undefined' ) { groupTypes . push ( GroupType [ v ] ) ; } } if ( groupTypes . length === 0 ) { return GroupType . any ; } return groupTypes . reduce ( ( a , c ) => a | c ) ; } } ) public get groupType ( ) : GroupType { return this . _groupType ; } public set groupType ( value ) { if ( this . _groupType === value ) { return ; } this . _groupType = value ; this . requestStateUpdate ( true ) ; } @ property ( { attribute : 'user-type' , converter : ( value , type ) => { value = value . toLowerCase ( ) ; if ( ! value || value . length === 0 ) { return UserType . any ; } if ( typeof UserType [ value ] === 'undefined' ) { return UserType . any ; } else { return UserType [ value ] ; } } } ) public get userType ( ) : UserType { return this . _userType ; } public set userType ( value ) { if ( this . _userType === value ) { return ; } this . _userType = value ; this . requestStateUpdate ( true ) ; } type { boolean } @ property ( { attribute : 'transitive-search' , type : Boolean } ) public transitiveSearch : boolean ; type { IDynamicPerson [ ] } @ property ( { attribute : 'people' , type : Object } ) public people : IDynamicPerson [ ] ; type { number } @ property ( { attribute : 'show-max' , type : Number } ) public showMax : number ; type { IDynamicPerson [ ] } @ property ( { attribute : 'selected-people' , type : Array } ) public selectedPeople : IDynamicPerson [ ] ; type { string [ ] } memberof @ property ( { attribute : 'default-selected-user-ids' , converter : value => { return value . split ( ',' ) . map ( v => v . trim ( ) ) ; } , type : String } ) public defaultSelectedUserIds : string [ ] ; type { string [ ] } memberof @ property ( { attribute : 'default-selected-group-ids' , converter : value => { return value . split ( ',' ) . map ( v => v . trim ( ) ) ; } , type : String } ) public defaultSelectedGroupIds : string [ ] ; type { string } memberof @ property ( { attribute : 'placeholder' , type : String } ) public placeholder : string ; type { boolean } memberof @ property ( { attribute : 'disabled' , type : Boolean } ) public disabled : boolean ; type { boolean } memberof @ property ( { attribute : 'allow-any-email' , type : Boolean } ) public allowAnyEmail : boolean ; type { string } memberof @ property ( { attribute : 'selection-mode' , type : String } ) public selectionMode : string ; private _userIds : string [ ] ; type { string [ ] } memberof @ property ( { attribute : 'user-ids' , converter : value => { return value . split ( ',' ) . map ( v => v . trim ( ) ) ; } , type : String } ) public get userIds ( ) : string [ ] { return this . _userIds ; } public set userIds ( value ) { if ( arraysAreEqual ( this . _userIds , value ) ) { return ; } this . _userIds = value ; this . requestStateUpdate ( true ) ; } static return { * } memberof public static get requiredScopes ( ) : string [ ] { return [ ... new Set ( [ 'user.read.all' , 'people.read' , 'group.read.all' , 'user.readbasic.all' , ... MgtPerson . requiredScopes ] ) ] ; } protected type { string } memberof protected userInput : string ; @ property ( { attribute : false } ) private _showLoading : boolean ; private _groupId : string ; private _type : PersonType = PersonType . person ; private _groupType : GroupType = GroupType . any ; private _userType : UserType = UserType . any ; private defaultPeople : IDynamicPerson [ ] ; private _arrowSelectionCount : number = 0 ; private _groupPeople : IDynamicPerson [ ] ; private _debouncedSearch : { ( ) : void ; ( ) : void ; } ; private defaultSelectedUsers : IDynamicPerson [ ] ; private defaultSelectedGroups : IDynamicPerson [ ] ; private _highlightedUsers : Element [ ] = [ ] ; private _currentHighlightedUserPos : number = 0 ; @ internalProperty ( ) private _isFocused = false ; @ internalProperty ( ) private _foundPeople : IDynamicPerson [ ] ; private _mouseLeaveTimeout ; private _mouseEnterTimeout ; private _isKeyboardFocus : boolean = true ; constructor ( ) { super ( ) ; this . clearState ( ) ; this . _showLoading = true ; this . showMax = 6 ; this . disabled = false ; this . allowAnyEmail = false ; this . addEventListener ( 'copy' , this . handleCopy ) ; this . addEventListener ( 'cut' , this . handleCut ) ; this . addEventListener ( 'paste' , this . handlePaste ) ; } param { FocusOptions } options memberof public focus ( options ? ) { this . gainedFocus ( ) ; if ( ! this . input ) { return ; } this . input . focus ( options ) ; this . input . select ( ) ; } param { readonly string [ ] } an returns { Promise < void > } memberof public async selectUsersById ( userIds ) { const provider = Providers . globalProvider ; const graph = Providers . globalProvider . graph ; if ( provider && provider . state === ProviderState . SignedIn ) { for ( const id in userIds ) { const userId = userIds [ id ] ; try { const personDetails = await getUser ( graph , userId ) ; this . addPerson ( personDetails ) ; } catch ( e ) { if ( e . message && e . message . includes ( 'does not exist' ) && this . allowAnyEmail ) { if ( isValidEmail ( userId ) ) { const anyMailUser = { mail : userId , displayName : userId } ; this . addPerson ( anyMailUser ) ; } } } } } } param { readonly string [ ] } an returns { Promise < void > } memberof public async selectGroupsById ( groupIds ) { const provider = Providers . globalProvider ; const graph = Providers . globalProvider . graph ; if ( provider && provider . state === ProviderState . SignedIn ) { for ( const id in groupIds ) { try { const groupDetails = await getGroup ( graph , groupIds [ id ] ) ; this . addPerson ( groupDetails ) ; } catch ( e ) { } } } } returns { TemplateResult } memberof public render ( ) { const defaultTemplate = this . renderTemplate ( 'default' , { people : this . _foundPeople } ) ; if ( defaultTemplate ) { return defaultTemplate ; } const selectedPeopleTemplate = this . renderSelectedPeople ( this . selectedPeople ) ; const inputTemplate = this . renderInput ( ) ; const flyoutTemplate = this . renderFlyout ( inputTemplate ) ; const inputClasses = { focused : this . _isFocused , 'people-picker' : true , disabled : this . disabled } ; return html `
       <div dir=${ this . direction } class=${ classMap ( inputClasses ) } @click=${ e => this . focus ( e ) }>
         <div class="selected-list">
           ${ selectedPeopleTemplate } ${ flyoutTemplate }
         </div>
       </div>
     ` ; } protected memberof protected clearState ( ) { this . _groupId = null ; this . selectedPeople = [ ] ; this . userInput = '' ; this . _highlightedUsers = [ ] ; this . _currentHighlightedUserPos = 0 ; } protected memberof protected requestStateUpdate ( force ? ) { if ( force ) { this . _groupPeople = null ; this . _foundPeople = null ; this . selectedPeople = [ ] ; } return super . requestStateUpdate ( force ) ; } protected returns { TemplateResult } memberof protected renderInput ( ) { const hasSelectedPeople = ! ! this . selectedPeople . length ; const placeholder = ! this . disabled ? this . placeholder ? this . placeholder : this . strings . inputPlaceholderText : this . placeholder || '' ; const selectionMode = this . selectionMode ? this . selectionMode : 'multiple' ; const inputClasses = { 'search-box' : true , 'search-box-start' : hasSelectedPeople } ; if ( selectionMode === 'single' && this . selectedPeople . length >= 1 ) { this . lostFocus ( ) ; return null ; } return html `
       <div class="${ classMap ( inputClasses ) }">
         <input
           id="people-picker-input"
           class="search-box__input"
           type="text"
           placeholder=${ placeholder }
           label="people-picker-input"
           aria-label="people-picker-input"
           role="input"
           @keydown="${ this . onUserKeyDown }"
           @keyup="${ this . onUserKeyUp }"
           @blur=${ this . lostFocus }
           @click=${ this . handleFlyout }
           ?disabled=${ this . disabled }
         />
       </div>
     ` ; } protected returns { TemplateResult } memberof protected renderSelectedPeople ( selectedPeople ? ) { selectedPeople = selectedPeople || this . selectedPeople ; if ( ! this . selectedPeople || ! this . selectedPeople . length ) { return null ; } return html `
       <div
        tabindex="0"
        aria-label="selected-people"
        aria-orientation="vertical"
        role="listbox"
        class="selected-list__options">${ selectedPeople . slice ( 0 , selectedPeople . length ) . map ( person => html `
             <div
             role="option"
             tabindex="0"
             aria-label=${ person . displayName }
             class="selected-list__person-wrapper">
               ${ this . renderTemplate ( 'selected-person' , { person } , `selected-${ person . id ? person . id : person . displayName }` ) || this . renderSelectedPerson ( person ) }

               <div class="selected-list__person-wrapper__overflow">
                 <div class="selected-list__person-wrapper__overflow__gradient"></div>
                 <div
                   tabindex="0"
                   aria-label="close-icon"
                   class="selected-list__person-wrapper__overflow__close-icon"
                   @click="${ e => this . removePerson ( person , e ) }"
                 >
                   \uE711
                 </div>
               </div>
             </div>
           ` ) }</div>
     ` ; } protected returns { TemplateResult } memberof protected renderFlyout ( anchor ) { return html `
       <mgt-flyout light-dismiss class="flyout">
         ${ anchor }
         <div slot="flyout" class="flyout-root" @wheel=${ ( e ) => this . handleSectionScroll ( e ) }>
           ${ this . renderFlyoutContent ( ) }
         </div>
       </mgt-flyout>
     ` ; } protected returns { TemplateResult } memberof protected renderFlyoutContent ( ) { if ( this . isLoadingState || this . _showLoading ) { return this . renderLoading ( ) ; } let people = this . _foundPeople ; if ( ! people || people . length === 0 || this . showMax === 0 ) { return this . renderNoData ( ) ; } for ( const person of people ) { ( person as IFocusable ) . isFocused = false ; } people = people . slice ( 0 , this . showMax ) ; ( people [ 0 ] as IFocusable ) . isFocused = true ; return this . renderSearchResults ( people ) ; } protected returns memberof protected renderLoading ( ) { return ( this . renderTemplate ( 'loading' , null ) || 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 returns { TemplateResult } memberof protected renderNoData ( ) { if ( ! this . _isFocused ) { return ; } return ( this . renderTemplate ( 'error' , null ) || this . renderTemplate ( 'no-data' , null ) || 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 param { IDynamicPerson [ ] } people returns memberof protected renderSearchResults ( people ? ) { people = people || this . _foundPeople ; let filteredPeople = people . filter ( person => person . id ) ; return html `
       <div class="people-list" @mouseenter=${ this . handleMouseEnter } @mouseleave=${ this . handleMouseLeave }>
         ${ repeat ( filteredPeople , person => person . id , person => { const listPersonClasses = { focused : this . _isKeyboardFocus && this . _arrowSelectionCount === 0 ? ( person as IFocusable ) . isFocused : '' , 'list-person' : true } ; return html `
               <li class="${ classMap ( listPersonClasses ) }" @click="${ e => this . onPersonClick ( person ) }">
                 ${ this . renderPersonResult ( person ) }
               </li>
             ` ; } ) }
       </div>
     ` ; } protected param { IDynamicPerson } person returns { TemplateResult } memberof protected renderPersonResult ( person ) { const user = person as User ; const subTitle = user . jobTitle || user . mail ; const classes = { 'people-person-job-title' : true , uppercase : ! ! user . jobTitle } ; return ( this . renderTemplate ( 'person' , { person } , person . id ) || html `
         <mgt-person .personDetails=${ person } .fetchImage=${ true }></mgt-person>
         <div class="people-person-text-area" id="${ person . displayName }">
           ${ this . renderHighlightText ( person ) }
           <span class="${ classMap ( classes ) }">${ subTitle }</span>
         </div>
       ` ) ; } protected param { IDynamicPerson } person returns { TemplateResult } memberof protected renderSelectedPerson ( person ) { return html `
       <mgt-person
         tabindex="-1"
         class="selected-list__person-wrapper__person"
         .personDetails=${ person }
         .fetchImage=${ true }
         .view=${ ViewType . oneline }
         .personCardInteraction=${ PersonCardInteraction . click }
       ></mgt-person>
     ` ; } /**
     * Async query to Graph for members of group if determined by developer.
     * set's `this.groupPeople` to those members.
     */ protected async loadState ( ) { let people = this . people ; const input = this . userInput . toLowerCase ( ) ; const provider = Providers . globalProvider ; if ( ! people && provider && provider . state === ProviderState . SignedIn ) { const graph = provider . graph . forComponent ( this ) ; if ( ! input . length && this . _isFocused ) { if ( this . defaultPeople ) { people = this . defaultPeople ; } else { if ( this . groupId ) { if ( this . _groupPeople === null ) { try { this . _groupPeople = await findGroupMembers ( graph , null , this . groupId , this . showMax , this . type , this . transitiveSearch ) ; } catch ( _ ) { this . _groupPeople = [ ] ; } } people = this . _groupPeople || [ ] ; } else if ( this . type === PersonType . person || this . type === PersonType . any ) { if ( this . userIds ) { people = await getUsersForUserIds ( graph , this . userIds ) ; } else { people = await getPeople ( graph , this . userType ) ; } } else if ( this . type === PersonType . group ) { let groups = ( await findGroups ( graph , '' , this . showMax , this . groupType ) ) || [ ] ; if ( groups [ 0 ] [ 'value' ] ) { groups = groups [ 0 ] [ 'value' ] ; } people = groups ; } this . defaultPeople = people ; } } this . _showLoading = false ; if ( ( this . defaultSelectedUserIds || this . defaultSelectedGroupIds ) && ! this . selectedPeople . length && ! this . defaultSelectedUsers ) { this . defaultSelectedUsers = await getUsersForUserIds ( graph , this . defaultSelectedUserIds ) ; this . defaultSelectedGroups = await getGroupsForGroupIds ( graph , this . defaultSelectedGroupIds ) ; this . defaultSelectedGroups = this . defaultSelectedGroups . filter ( group => { return group !== null ; } ) ; this . defaultSelectedUsers = this . defaultSelectedUsers . filter ( user => { return user !== null ; } ) ; this . selectedPeople = [ ... this . defaultSelectedUsers , ... this . defaultSelectedGroups ] ; this . requestUpdate ( ) ; this . fireCustomEvent ( 'selectionChanged' , this . selectedPeople ) ; } if ( input ) { people = [ ] ; if ( this . groupId ) { people = ( await findGroupMembers ( graph , input , this . groupId , this . showMax , this . type , this . transitiveSearch ) ) || [ ] ; } else { if ( this . type === PersonType . person || this . type === PersonType . any ) { try { if ( this . userIds && this . userIds . length ) { people = await getUsersForUserIds ( graph , this . userIds , input ) ; } else { people = ( await findPeople ( graph , input , this . showMax , this . userType ) ) || [ ] ; } } catch ( e ) { } if ( people . length < this . showMax && this . userType !== UserType . contact ) { try { const users = ( await findUsers ( graph , input , this . showMax ) ) || [ ] ; const peopleIds = new Set ( people . map ( p => p . id ) ) ; for ( const user of users ) { if ( ! peopleIds . has ( user . id ) ) { people . push ( user ) ; } } } catch ( e ) { } } } if ( ( this . type === PersonType . group || this . type === PersonType . any ) && people . length < this . showMax ) { let groups = [ ] ; try { groups = ( await findGroups ( graph , input , this . showMax , this . groupType ) ) || [ ] ; people = people . concat ( groups ) ; } catch ( e ) { } } } } } this . _foundPeople = this . filterPeople ( people ) ; } protected memberof protected hideFlyout ( ) { const flyout = this . flyout ; if ( flyout ) { flyout . close ( ) ; } } protected memberof protected showFlyout ( ) { const flyout = this . flyout ; if ( flyout ) { flyout . open ( ) ; } } param person protected removePerson ( person , e ) { e . stopPropagation ( ) ; const filteredPersonArr = this . selectedPeople . filter ( p => { if ( ! person . id && p . displayName ) { return p . displayName !== person . displayName ; } return p . id !== person . id ; } ) ; this . selectedPeople = filteredPersonArr ; this . loadState ( ) ; this . fireCustomEvent ( 'selectionChanged' , this . selectedPeople ) ; } param person param event protected addPerson ( person ) { if ( person ) { setTimeout ( ( ) => { this . clearInput ( ) ; } , 50 ) ; const duplicatePeople = this . selectedPeople . filter ( p => { if ( ! person . id && p . displayName ) { return p . displayName === person . displayName ; } return p . id === person . id ; } ) ; if ( duplicatePeople . length === 0 ) { this . selectedPeople = [ ... this . selectedPeople , person ] ; this . fireCustomEvent ( 'selectionChanged' , this . selectedPeople ) ; this . loadState ( ) ; this . _foundPeople = [ ] ; } } } private clearInput ( ) { this . clearHighlighted ( ) ; if ( this . selectionMode !== 'single' ) { this . input . value = '' ; } this . userInput = '' ; } private handleFlyout ( ) { const peopleLeft = this . filterPeople ( this . defaultPeople ) ; let shouldShow = true ; if ( peopleLeft && peopleLeft . length === 0 ) { shouldShow = false ; } if ( shouldShow ) { window . requestAnimationFrame ( ( ) => { this . showFlyout ( ) ; } ) ; } } private gainedFocus ( ) { this . clearHighlighted ( ) ; this . _isFocused = true ; if ( this . input ) { this . input . focus ( ) ; } this . _showLoading = true ; this . loadState ( ) ; } private lostFocus ( ) { this . _isFocused = false ; this . requestUpdate ( ) ; } private handleMouseEnter ( e ) { clearTimeout ( this . _mouseEnterTimeout ) ; clearTimeout ( this . _mouseLeaveTimeout ) ; this . _mouseEnterTimeout = setTimeout ( this . hideKeyboardFocus . bind ( this ) , 100 ) ; } private handleMouseLeave ( e ) { clearTimeout ( this . _mouseEnterTimeout ) ; clearTimeout ( this . _mouseLeaveTimeout ) ; this . _mouseLeaveTimeout = setTimeout ( this . showKeyboardFocus . bind ( this ) , 100 ) ; } private hideKeyboardFocus ( ) { this . _isKeyboardFocus = false ; this . requestUpdate ( ) ; const peopleList = this . renderRoot . querySelector ( '.people-list' ) ; if ( peopleList && peopleList . children . length ) { for ( let i = 0 ; i < peopleList . children . length ; i ++ ) { peopleList . children [ i ] . classList . remove ( 'focused' ) ; } } } private showKeyboardFocus ( ) { this . _isKeyboardFocus = true ; this . handleArrowSelection ( ) ; } private renderHighlightText ( person ) { let first = '' ; let last = '' ; let highlight = '' ; const displayName = person . displayName ; const highlightLocation = displayName . toLowerCase ( ) . indexOf ( this . userInput . toLowerCase ( ) ) ; if ( highlightLocation !== - 1 ) { const userInputLength = this . userInput . length ; if ( highlightLocation === 0 ) { first = '' ; highlight = displayName . slice ( 0 , userInputLength ) ; last = displayName . slice ( userInputLength , displayName . length ) ; } else if ( highlightLocation === displayName . length ) { first = displayName . slice ( 0 , highlightLocation ) ; highlight = displayName . slice ( highlightLocation , displayName . length ) ; last = '' ; } else { first = displayName . slice ( 0 , highlightLocation ) ; highlight = displayName . slice ( highlightLocation , highlightLocation + userInputLength ) ; last = displayName . slice ( highlightLocation + userInputLength , displayName . length ) ; } } else { first = person . displayName ; } return html `
       <div>
         <span class="people-person-text">${ first }</span
         ><span class="people-person-text highlight-search-text">${ highlight }</span
         ><span class="people-person-text">${ last }</span>
       </div>
     ` ; } /**
     * Adds debounce method for set delay on user input
     */ private onUserKeyUp ( event ) { const isCmdOrCtrlKey = [ 'ControlLeft' , 'ControlRight' ] . includes ( event . code ) || event . ctrlKey || event . metaKey ; const isArrowKey = [ 'ArrowDown' , 'ArrowRight' , 'ArrowUp' , 'ArrowLeft' ] . includes ( event . code ) ; if ( isCmdOrCtrlKey || isArrowKey ) { if ( isCmdOrCtrlKey || [ 'ArrowLeft' , 'ArrowRight' ] . includes ( event . code ) ) { this . hideFlyout ( ) ; } return ; } if ( event . code === 'Tab' && ! this . flyout . isOpen ) { if ( this . allowAnyEmail ) { this . gainedFocus ( ) ; } } if ( event . shiftKey ) { this . gainedFocus ( ) ; } const input = event . target as HTMLInputElement ; if ( event . code === 'Escape' ) { this . clearInput ( ) ; this . _foundPeople = [ ] ; } else if ( event . code === 'Backspace' && this . userInput . length === 0 && this . selectedPeople . length > 0 ) { this . clearHighlighted ( ) ; this . selectedPeople = this . selectedPeople . splice ( 0 , this . selectedPeople . length - 1 ) ; this . loadState ( ) ; this . hideFlyout ( ) ; this . fireCustomEvent ( 'selectionChanged' , this . selectedPeople ) ; } else if ( event . code === 'Comma' || event . code === 'Semicolon' ) { if ( this . allowAnyEmail ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; } return ; } else { this . userInput = input . value ; const validEmail = isValidEmail ( this . userInput ) ; if ( ! validEmail ) { this . handleUserSearch ( ) ; } } } private handleAnyEmail ( ) { this . _showLoading = false ; this . _arrowSelectionCount = 0 ; if ( isValidEmail ( this . userInput ) ) { const anyMailUser = { mail : this . userInput , displayName : this . userInput } ; this . addPerson ( anyMailUser ) ; } this . hideFlyout ( ) ; if ( this . input ) { this . input . focus ( ) ; this . _isFocused = true ; } } private onPersonClick ( person ) { this . addPerson ( person ) ; this . hideFlyout ( ) ; if ( ! this . input ) { return ; } this . input . focus ( ) ; this . _isFocused = true ; this . hideFlyout ( ) ; if ( this . selectionMode === 'single' ) { return ; } } param input private handleUserSearch ( ) { if ( ! this . _debouncedSearch ) { this . _debouncedSearch = debounce ( async ( ) => { const loadingTimeout = setTimeout ( ( ) => { this . _showLoading = true ; } , 50 ) ; await this . loadState ( ) ; clearTimeout ( loadingTimeout ) ; this . _showLoading = false ; this . showFlyout ( ) ; this . _arrowSelectionCount = 0 ; } , 400 ) ; } this . _debouncedSearch ( ) ; } param event private onUserKeyDown ( event ) { const selectedList = this . renderRoot . querySelector ( '.selected-list' ) ; const isCmdOrCtrlKey = event . ctrlKey || event . metaKey ; if ( isCmdOrCtrlKey && selectedList ) { const selectedPeople = selectedList . querySelectorAll ( 'mgt-person.selected-list__person-wrapper__person' ) ; this . hideFlyout ( ) ; if ( isCmdOrCtrlKey && event . code === 'ArrowLeft' ) { this . _currentHighlightedUserPos = ( this . _currentHighlightedUserPos - 1 + selectedPeople . length ) % selectedPeople . length ; if ( this . _currentHighlightedUserPos >= 0 && this . _currentHighlightedUserPos !== NaN ) { this . _highlightedUsers . push ( selectedPeople [ this . _currentHighlightedUserPos ] ) ; } else { this . _currentHighlightedUserPos = 0 ; } } else if ( isCmdOrCtrlKey && event . code === 'ArrowRight' ) { const person = this . _highlightedUsers . pop ( ) ; if ( person ) { const personParent = person . parentElement ; if ( personParent ) { this . clearHighlighted ( personParent ) ; this . _currentHighlightedUserPos ++ ; } } } else if ( isCmdOrCtrlKey && event . code === 'KeyA' ) { this . _highlightedUsers = [ ] ; selectedPeople . forEach ( person => this . _highlightedUsers . push ( person ) ) ; } if ( this . _highlightedUsers ) { this . highlightSelectedPeople ( this . _highlightedUsers ) ; } return ; } this . clearHighlighted ( ) ; if ( ! this . flyout . isOpen ) { return ; } if ( event . keyCode === 40 || event . keyCode === 38 ) { this . handleArrowSelection ( event ) ; if ( this . input . value . length > 0 ) { event . preventDefault ( ) ; } } const input = event . target as HTMLInputElement ; if ( event . code === 'Tab' || event . code === 'Enter' ) { if ( ! event . shiftKey && this . _foundPeople ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; if ( this . _foundPeople . length ) { this . fireCustomEvent ( 'blur' ) ; } const foundPerson = this . _foundPeople [ this . _arrowSelectionCount ] ; if ( foundPerson ) { this . addPerson ( foundPerson ) ; } else if ( this . allowAnyEmail ) { this . handleAnyEmail ( ) ; } } this . hideFlyout ( ) ; ( event . target as HTMLInputElement ) . value = '' ; } else if ( event . code === 'Comma' || event . code === 'Semicolon' ) { if ( this . allowAnyEmail ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; this . userInput = input . value ; this . handleAnyEmail ( ) ; } } } /**
     * Gets the text of the highlighed people and writes it to the clipboard
     */ private async writeHighlightedText ( ) { const copyText = [ ] ; for ( let i = 0 ; i < this . _highlightedUsers . length ; i ++ ) { const element = this . _highlightedUsers [ i ] ; const _personDetails = element . _personDetails ; const { id , displayName , email , userPrincipalName , scoredEmailAddresses } = _personDetails ; let emailAddress ; if ( scoredEmailAddresses && scoredEmailAddresses . length > 0 ) { emailAddress = scoredEmailAddresses . pop ( ) . address ; } else { emailAddress = userPrincipalName || email ; } copyText . push ( { id , displayName , email : emailAddress } ) ; } let copiedTextStr = '' ; if ( copyText . length > 0 ) { copiedTextStr = JSON . stringify ( copyText ) ; } await navigator . clipboard . writeText ( copiedTextStr ) ; } /**
     * Handles the cut event when it is fired
     */ private async handleCut ( ) { await this . writeHighlightedText ( ) ; this . removeHighlightedOnCut ( ) ; } /**
     * Handles the copy event when it is fired
     */ private async handleCopy ( ) { await this . writeHighlightedText ( ) ; } /**
     * Parses the copied people text and adds them when you paste
     */ private async handlePaste ( ) { try { const copiedText = await navigator . clipboard . readText ( ) ; if ( copiedText ) { try { const people = JSON . parse ( copiedText ) ; if ( people && people . length > 0 ) { for ( const person of people ) { this . addPerson ( person ) ; } } } catch ( error ) { if ( error instanceof SyntaxError ) { const _delimeters = [ ',' , ';' ] ; let listOfUsers ; try { for ( let i = 0 ; i < _delimeters . length ; i ++ ) { listOfUsers = copiedText . split ( _delimeters [ i ] ) ; if ( listOfUsers . length > 1 ) { this . hideFlyout ( ) ; this . selectUsersById ( listOfUsers ) ; break ; } } } catch ( error ) { } } } } } catch ( error ) { } } /**
     * Removes only the highlighted elements from the peoplePicker during cut operations.
     */ private removeHighlightedOnCut ( ) { this . selectedPeople = this . selectedPeople . splice ( 0 , this . selectedPeople . length - this . _highlightedUsers . length ) ; this . _highlightedUsers = [ ] ; this . _currentHighlightedUserPos = 0 ; this . loadState ( ) ; this . hideFlyout ( ) ; this . fireCustomEvent ( 'selectionChanged' , this . selectedPeople ) ; } param people private highlightSelectedPeople ( people ) { for ( let i = 0 ; i < people . length ; i ++ ) { const person = people [ i ] ; const parentElement = person . parentElement ; parentElement . setAttribute ( 'class' , 'selected-list__person-wrapper-highlighted' ) ; const personNodes = Array . from ( parentElement . getElementsByClassName ( 'selected-list__person-wrapper__person' ) ) ; if ( personNodes && personNodes . length > 0 ) { const personNode = personNodes . pop ( ) ; personNode . setAttribute ( 'class' , 'selected-list__person-wrapper-highlighted__person' ) ; } const gradientNodes = Array . from ( parentElement . getElementsByClassName ( 'selected-list__person-wrapper__overflow__gradient' ) ) ; if ( gradientNodes && gradientNodes . length > 0 ) { const gradientNode = gradientNodes . pop ( ) ; gradientNode . setAttribute ( 'class' , 'selected-list__person-wrapper-highlighted__overflow__gradient' ) ; } const closeIconNodes = Array . from ( parentElement . getElementsByClassName ( 'selected-list__person-wrapper__overflow__close-icon' ) ) ; if ( closeIconNodes && closeIconNodes . length > 0 ) { const closeIconNode = closeIconNodes . pop ( ) ; closeIconNode . setAttribute ( 'class' , 'selected-list__person-wrapper-highlighted__overflow__close-icon' ) ; } } } /**
     * Defaults the people class back to the normal view
     */ private clearHighlighted ( node ? ) { if ( node ) { this . clearNodeHighlights ( node ) ; } else { for ( let i = 0 ; i < this . _highlightedUsers . length ; i ++ ) { const person = this . _highlightedUsers [ i ] ; const parentElement = person . parentElement ; if ( parentElement ) { this . clearNodeHighlights ( parentElement ) ; } } this . _highlightedUsers = [ ] ; this . _currentHighlightedUserPos = 0 ; } } param node private clearNodeHighlights ( node ) { node . setAttribute ( 'class' , 'selected-list__person-wrapper' ) ; const personNodes = Array . from ( node . getElementsByClassName ( 'selected-list__person-wrapper-highlighted__person' ) ) ; if ( personNodes && personNodes . length > 0 ) { const personNode = personNodes . pop ( ) ; personNode . setAttribute ( 'class' , 'selected-list__person-wrapper__person' ) ; } const gradientNodes = Array . from ( node . getElementsByClassName ( 'selected-list__person-wrapper-highlighted__overflow__gradient' ) ) ; if ( gradientNodes && gradientNodes . length > 0 ) { const gradientNode = gradientNodes . pop ( ) ; gradientNode . setAttribute ( 'class' , 'selected-list__person-wrapper__overflow__gradient' ) ; } const closeIconNodes = Array . from ( node . getElementsByClassName ( 'selected-list__person-wrapper-highlighted__overflow__close-icon' ) ) ; if ( closeIconNodes && closeIconNodes . length > 0 ) { const closeIconNode = closeIconNodes . pop ( ) ; closeIconNode . setAttribute ( 'class' , 'selected-list__person-wrapper__overflow__close-icon' ) ; } } param event private handleArrowSelection ( event ? ) { const peopleList = this . renderRoot . querySelector ( '.people-list' ) ; if ( this . _isKeyboardFocus === false ) { return ; } if ( peopleList && peopleList . children . length ) { if ( event ) { if ( event . keyCode === 38 ) { this . _arrowSelectionCount = ( this . _arrowSelectionCount - 1 + peopleList . children . length ) % peopleList . children . length ; } if ( event . keyCode === 40 ) { this . _arrowSelectionCount = ( this . _arrowSelectionCount + 1 ) % peopleList . children . length ; } } for ( let i = 0 ; i < peopleList . children . length ; i ++ ) { peopleList . children [ i ] . classList . remove ( 'focused' ) ; } const focusedItem = peopleList . children [ this . _arrowSelectionCount ] ; if ( focusedItem ) { focusedItem . classList . add ( 'focused' ) ; focusedItem . scrollIntoView ( { behavior : 'smooth' , block : 'nearest' , inline : 'start' } ) ; } } } param people private filterPeople ( people ) { if ( people ) { const idFilter = this . selectedPeople . map ( el => { return el . id ? el . id : el . displayName ; } ) ; const filtered = people . filter ( ( person ) => { if ( person . id ) { return idFilter . indexOf ( person . id ) === - 1 ; } else { return idFilter . indexOf ( person . displayName ) === - 1 ; } } ) ; return filtered ; } } private handleSectionScroll ( e ) { const target = this . renderRoot . querySelector ( '.flyout-root' ) as HTMLElement ; if ( target ) { if ( ! ( e . deltaY < 0 && target . scrollTop === 0 ) && ! ( e . deltaY > 0 && target . clientHeight + target . scrollTop >= target . scrollHeight - 1 ) ) { e . stopPropagation ( ) ; } } } }