@microsoft/mgt
Version:
The Microsoft Graph Toolkit
546 lines • 20.6 kB
JavaScript
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { customElement, html, property } from 'lit-element';
import { repeat } from 'lit-html/directives/repeat';
import { Providers } from '../../Providers';
import { ProviderState } from '../../providers/IProvider';
import '../../styles/fabric-icon-font';
import { debounce } from '../../utils/Utils';
import '../mgt-person/mgt-person';
import { MgtTemplatedComponent } from '../templatedComponent';
import { styles } from './mgt-people-picker-css';
/**
* Web component used to search for people from the Microsoft Graph
*
* @export
* @class MgtPicker
* @extends {MgtTemplatedComponent}
*/
let MgtPeoplePicker = class MgtPeoplePicker extends MgtTemplatedComponent {
constructor() {
super();
/**
* containing object of MgtPersonDetails.
* @type {MgtPersonDetails}
*/
this.people = null;
/**
* determining how many people to show in list.
* @type {number}
*/
this.showMax = 6;
/**
* array of user picked people.
* @type {Array<any>}
*/
this.selectedPeople = [];
// User input in search
this._userInput = '';
// if search is still loading don't load "people not found" state
this.showLoading = false;
this.isLoading = false;
// tracking of user arrow key input for selection
this.arrowSelectionCount = 0;
}
/**
* Array of styles to apply to the element. The styles should be defined
* user the `css` tag function.
*/
static get styles() {
return styles;
}
/**
* Synchronizes property values when attributes change.
*
* @param {*} name
* @param {*} oldValue
* @param {*} newValue
* @memberof MgtPersonCard
*/
attributeChangedCallback(att, oldval, newval) {
super.attributeChangedCallback(att, oldval, newval);
if (att === 'group-id' && oldval !== newval) {
this.findGroup();
}
}
/**
* Invoked when the element is first updated. Implement to perform one time work on the element after update.
* Checks if group-id is present
* @memberof MgtPeoplePicker
*/
firstUpdated() {
if (this.groupId) {
Providers.onProviderUpdated(() => this.findGroup());
this.findGroup();
}
}
/**
* Invoked on each update to perform rendering tasks. This method must return a lit-html TemplateResult.
* Setting properties inside this method will not trigger the element to update.
* @returns
* @memberof MgtPeoplePicker
*/
render() {
return (this.renderTemplate('default', { people: this.people }) ||
html `
<div class="people-picker" @blur=${this.lostFocus}>
<div class="people-picker-input" @click=${this.gainedFocus}>
${this.renderSelectedPeople()}
</div>
<div class="people-list-separator"></div>
${this.renderPeopleList()}
</div>
`);
}
/**
* Focuses the input element when focus is called
*
* @param {FocusOptions} [options]
* @memberof MgtPeoplePicker
*/
focus(options) {
const peopleInput = this.renderRoot.querySelector('.people-selected-input');
if (peopleInput) {
peopleInput.focus(options);
}
}
/**
* Queries the microsoft graph for a user based on the user id and adds them to the selectedPeople array
*
* @param {[string]} an array of user ids to add to selectedPeople
* @returns {Promise<void>}
* @memberof MgtPeoplePicker
*/
selectUsersById(userIds) {
return __awaiter(this, void 0, void 0, function* () {
const provider = Providers.globalProvider;
const client = Providers.globalProvider.graph;
if (provider && provider.state === ProviderState.SignedIn) {
// tslint:disable-next-line: forin
for (const id in userIds) {
try {
const personDetails = yield client.getUser(userIds[id]);
this.addPerson(personDetails);
// tslint:disable-next-line: no-empty
}
catch (e) { }
}
}
});
}
/**
* Adds debounce method for set delay on user input
*/
onUserKeyUp(event) {
if (event.keyCode === 40 || event.keyCode === 38) {
// keyCodes capture: down arrow (40) and up arrow (38)
return;
}
const input = event.target;
if (event.code === 'Escape') {
input.value = '';
this._userInput = '';
this.people = [];
return;
}
if (event.code === 'Backspace' && this._userInput.length === 0 && this.selectedPeople.length > 0) {
input.value = '';
this._userInput = '';
// remove last person in selected list
this.selectedPeople = this.selectedPeople.splice(0, this.selectedPeople.length - 1);
// fire selected people changed event
this.fireCustomEvent('selectionChanged', this.selectedPeople);
return;
}
this.handleUserSearch(input);
}
/**
* Async query to Graph for members of group if determined by developer.
* set's `this.groupPeople` to those members.
*/
findGroup() {
return __awaiter(this, void 0, void 0, function* () {
const provider = Providers.globalProvider;
if (provider && provider.state === ProviderState.SignedIn) {
const client = Providers.globalProvider.graph;
this.groupPeople = yield client.getPeopleFromGroup(this.groupId);
}
});
}
/**
* Tracks event on user input in search
* @param input - input text
*/
handleUserSearch(input) {
if (!this.debouncedSearch) {
this.debouncedSearch = debounce(() => {
if (this._userInput !== input.value) {
this._userInput = input.value;
this.loadPersonSearch(this._userInput);
this.arrowSelectionCount = 0;
}
}, 200);
}
this.debouncedSearch();
}
/**
* Tracks event on user search (keydown)
* @param event - event tracked on user input (keydown)
*/
onUserKeyDown(event) {
if (event.keyCode === 40 || event.keyCode === 38) {
// keyCodes capture: down arrow (40) and up arrow (38)
this.handleArrowSelection(event);
if (this._userInput.length > 0) {
event.preventDefault();
}
}
if (event.code === 'Tab' || event.code === 'Enter') {
if (this.people.length) {
event.preventDefault();
}
this.addPerson(this.people[this.arrowSelectionCount]);
event.target.value = '';
}
}
/**
* Tracks user key selection for arrow key selection of people
* @param event - tracks user key selection
*/
handleArrowSelection(event) {
if (this.people.length) {
// update arrow count
if (event.keyCode === 38) {
// up arrow
if (this.arrowSelectionCount > 0) {
this.arrowSelectionCount--;
}
else {
this.arrowSelectionCount = 0;
}
}
if (event.keyCode === 40) {
// down arrow
if (this.arrowSelectionCount + 1 !== this.people.length && this.arrowSelectionCount + 1 < this.showMax) {
this.arrowSelectionCount++;
}
else {
this.arrowSelectionCount = 0;
}
}
const peopleList = this.renderRoot.querySelector('.people-list');
// reset background color
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < peopleList.children.length; i++) {
peopleList.children[i].setAttribute('class', 'list-person people-person-list');
}
// set selected background
peopleList.children[this.arrowSelectionCount].setAttribute('class', 'list-person people-person-list-fill');
}
}
/**
* Tracks when user selects person from picker
* @param person - contains details pertaining to selected user
* @param event - tracks user event
*/
addPerson(person) {
if (person) {
this._userInput = '';
const duplicatePeople = this.selectedPeople.filter(p => {
return p.id === person.id;
});
if (duplicatePeople.length === 0) {
this.selectedPeople.push(person);
this.fireCustomEvent('selectionChanged', this.selectedPeople);
this.people = [];
}
}
this.gainedFocus();
}
/**
* Async method which query's the Graph with user input
* @param name - user input or name of person searched
*/
loadPersonSearch(name) {
return __awaiter(this, void 0, void 0, function* () {
if (name.length) {
name = name.toLowerCase();
const provider = Providers.globalProvider;
let people;
if (provider && provider.state === ProviderState.SignedIn) {
this.isLoading = true;
setTimeout(() => {
this.showLoading = this.isLoading;
}, 400);
const client = Providers.globalProvider.graph;
// filtering groups
if (this.groupId) {
people = this.groupPeople;
}
else {
people = yield client.findPerson(name);
}
if (people) {
people = people.filter(person => {
return person.displayName.toLowerCase().indexOf(name) !== -1;
});
}
this.people = this.filterPeople(people);
this.isLoading = false;
this.showLoading = false;
}
}
else {
this.people = [];
}
});
}
/**
* Filters people searched from already selected people
* @param people - array of people returned from query to Graph
*/
filterPeople(people) {
// check if people need to be updated
// ensuring people list is displayed
// find ids from selected people
if (people) {
const idFilter = this.selectedPeople.map(el => {
return el.id;
});
// filter id's
const filtered = people.filter(person => {
return idFilter.indexOf(person.id) === -1;
});
return filtered;
}
}
/**
* Removes person from selected people
* @param person - person and details pertaining to user selected
*/
removePerson(person) {
const filteredPersonArr = this.selectedPeople.filter(p => {
return p.id !== person.id;
});
this.selectedPeople = filteredPersonArr;
this.fireCustomEvent('selectionChanged', this.selectedPeople);
}
renderErrorMessage() {
return html `
<div class="message-parent">
<div label="search-error-text" aria-label="We didn't find any matches." class="search-error-text">
We didn't find any matches.
</div>
</div>
`;
}
renderLoadingMessage() {
return html `
<div class="message-parent">
<div label="search-error-text" aria-label="loading" class="loading-text">
......
</div>
</div>
`;
}
renderSelectedPeople() {
let peopleList;
let inputClass = 'input-search-start';
if (this.selectedPeople.length > 0) {
inputClass = 'input-search';
peopleList = html `
${this.selectedPeople.slice(0, this.selectedPeople.length).map(person => html `
<div class="people-person">
${this.renderTemplate('selected-person', { person }, `selected-${person.id}`) ||
this.renderSelectedPerson(person)}
<div class="CloseIcon" @click="${() => this.removePerson(person)}">\uE711</div>
</div>
`)}
`;
}
else {
peopleList = null;
}
// tslint:disable
return html `
<div class="people-selected-list">
${peopleList}
<div class="${inputClass}">
<input
id="people-picker-input"
class="people-selected-input"
type="text"
placeholder="Start typing a name"
label="people-picker-input"
aria-label="people-picker-input"
role="input"
.value="${this._userInput}"
@keydown="${this.onUserKeyDown}"
@keyup="${this.onUserKeyUp}"
/>
</div>
</div>
`;
}
// tslint:enable
gainedFocus() {
const peopleList = this.renderRoot.querySelector('.people-list');
const peopleInput = this.renderRoot.querySelector('.people-selected-input');
peopleInput.focus();
peopleInput.select();
if (peopleList) {
// Mouse is focused on input
peopleList.setAttribute('style', 'display:block');
}
}
lostFocus() {
const peopleList = this.renderRoot.querySelector('.people-list');
if (peopleList) {
peopleList.setAttribute('style', 'display:none');
}
}
renderHighlightText(person) {
const peoples = person;
const highlightLocation = peoples.displayName.toLowerCase().indexOf(this._userInput.toLowerCase());
if (highlightLocation !== -1) {
// no location
if (highlightLocation === 0) {
// highlight is at the beginning of sentence
peoples.first = '';
peoples.highlight = peoples.displayName.slice(0, this._userInput.length);
peoples.last = peoples.displayName.slice(this._userInput.length, peoples.displayName.length);
}
else if (highlightLocation === peoples.displayName.length) {
// highlight is at end of the sentence
peoples.first = peoples.displayName.slice(0, highlightLocation);
peoples.highlight = peoples.displayName.slice(highlightLocation, peoples.displayName.length);
peoples.last = '';
}
else {
// highlight is in middle of sentence
peoples.first = peoples.displayName.slice(0, highlightLocation);
peoples.highlight = peoples.displayName.slice(highlightLocation, highlightLocation + this._userInput.length);
peoples.last = peoples.displayName.slice(highlightLocation + this._userInput.length, peoples.displayName.length);
}
}
return html `
<div>
<span class="people-person-text">${peoples.first}</span
><span class="people-person-text highlight-search-text">${peoples.highlight}</span
><span class="people-person-text">${peoples.last}</span>
</div>
`;
}
renderPeopleList() {
let content;
if (this.showLoading) {
content = this.renderTemplate('loading', null, 'loading') || this.renderLoadingMessage();
}
else if (this.people) {
const people = this.people.slice(0, this.showMax);
if (!this.isLoading && people.length === 0 && this._userInput.length > 0) {
content = this.renderTemplate('error', null, 'error') || this.renderErrorMessage();
}
else {
if (people[0]) {
people[0].isSelected = 'fill';
}
content = this.renderPeople(people);
}
}
return html `
<div class="people-list">
${content}
</div>
`;
}
renderPeople(people) {
return html `
${repeat(people, person => person.id, person => html `
<li
class="list-person ${person.isSelected === 'fill' ? 'people-person-list-fill' : 'people-person-list'}"
@click="${() => this.addPerson(person)}"
>
${this.renderTemplate('person', { person }, person.id) || this.renderPerson(person)}
</li>
`)}
`;
}
renderPerson(person) {
return html `
<mgt-person .personDetails=${person} .personImage=${'@'}></mgt-person>
<div class="people-person-text-area" id="${person.displayName}">
${this.renderHighlightText(person)}
<span class="people-person-job-title">${person.jobTitle}</span>
</div>
`;
}
renderSelectedPerson(person) {
return html `
<mgt-person
class="selected-person"
.personDetails=${person}
.personImage=${'@'}
show-name
person-card="click"
></mgt-person>
`;
}
};
__decorate([
property({
attribute: 'people',
type: Object
})
], MgtPeoplePicker.prototype, "people", void 0);
__decorate([
property({
attribute: 'show-max',
type: Number
})
], MgtPeoplePicker.prototype, "showMax", void 0);
__decorate([
property({
attribute: 'group-id',
type: String
})
], MgtPeoplePicker.prototype, "groupId", void 0);
__decorate([
property({
attribute: 'selected-people',
type: Array
})
], MgtPeoplePicker.prototype, "selectedPeople", void 0);
__decorate([
property()
], MgtPeoplePicker.prototype, "_userInput", void 0);
__decorate([
property()
], MgtPeoplePicker.prototype, "showLoading", void 0);
__decorate([
property()
], MgtPeoplePicker.prototype, "isLoading", void 0);
MgtPeoplePicker = __decorate([
customElement('mgt-people-picker')
], MgtPeoplePicker);
export { MgtPeoplePicker };
//# sourceMappingURL=mgt-people-picker.js.map