novo-elements
Version:
1 lines • 139 kB
Source Map (JSON)
{"version":3,"file":"novo-elements-elements-picker.mjs","sources":["../../../projects/novo-elements/src/elements/picker/extras/base-picker-results/BasePickerResults.ts","../../../projects/novo-elements/src/elements/picker/extras/checklist-picker-results/ChecklistPickerResults.ts","../../../projects/novo-elements/src/elements/picker/extras/distributionlist-picker-results/DistributionListPickerResults.ts","../../../projects/novo-elements/src/elements/picker/extras/entity-picker-results/EntityPickerResults.ts","../../../projects/novo-elements/src/elements/picker/extras/grouped-multi-picker-results/GroupedMultiPickerResults.ts","../../../projects/novo-elements/src/elements/picker/extras/mixed-multi-picker-results/MixedMultiPickerResults.ts","../../../projects/novo-elements/src/elements/picker/extras/picker-results/PickerResults.ts","../../../projects/novo-elements/src/elements/picker/extras/skills-picker-results/SkillsSpecialtyPickerResults.ts","../../../projects/novo-elements/src/elements/picker/extras/workers-comp-codes-picker-results/WorkersCompCodesPickerResults.ts","../../../projects/novo-elements/src/elements/picker/Picker.ts","../../../projects/novo-elements/src/elements/picker/Picker.module.ts","../../../projects/novo-elements/src/elements/picker/novo-elements-elements-picker.ts"],"sourcesContent":["// NG2\nimport { OverlayRef } from '@angular/cdk/overlay';\nimport { ChangeDetectorRef, Directive, ElementRef, Input } from '@angular/core';\n// Vendor\nimport { from, Observable } from 'rxjs';\n// APP\nimport { Helpers } from 'novo-elements/utils';\n\n/**\n * @description This is the actual list of matches that gets injected into the DOM. It's also the piece that can be\n * overwritten if custom list options are needed.\n */\n@Directive()\nexport class BasePickerResults {\n _term: string = '';\n selected: Array<any> = [];\n hasError: boolean = false;\n isLoading: boolean = false;\n isStatic: boolean = true;\n _config: any;\n activeMatch: any;\n parent: any;\n element: ElementRef;\n ref: ChangeDetectorRef;\n page: number = 0;\n lastPage: boolean = false;\n autoSelectFirstOption: boolean = true;\n overlay: OverlayRef;\n optionsFunctionHasChanged: boolean = false;\n private selectingMatches: boolean = false;\n private scrollHandler: any;\n _matches: Array<any> = [];\n customTextValue: any = null;\n\n @Input()\n set matches(m: Array<any>) {\n this._matches = m;\n }\n\n get matches() {\n return this.customTextValue ? [...this._matches, this.customTextValue] : this._matches;\n }\n\n constructor(element: ElementRef, ref: ChangeDetectorRef) {\n this.element = element;\n this.ref = ref;\n this.scrollHandler = this.onScrollDown.bind(this);\n }\n\n cleanUp(): void {\n const element: Element = this.getListElement();\n if (element && element.hasAttribute('scrollListener')) {\n element.removeAttribute('scrollListener');\n element.removeEventListener('scroll', this.scrollHandler);\n }\n }\n\n onScrollDown(event: WheelEvent) {\n const element: any = event.target;\n if (element) {\n const offset = element.offsetHeight + element.scrollTop;\n const bottom = element.scrollHeight - 300;\n if (offset >= bottom) {\n event.stopPropagation();\n if (!this.lastPage && !this.isLoading) {\n this.processSearch();\n }\n }\n }\n }\n\n @Input()\n set term(value) {\n if (this.shouldSearch(value)) {\n this._term = value;\n this.page = 0;\n this.optionsFunctionHasChanged = false;\n this.matches = [];\n this.processSearch(true);\n } else {\n this.addScrollListener();\n }\n }\n\n get term() {\n return this._term;\n }\n\n set config(value: any) {\n if (this.config && this.config.options !== value.options) {\n this.optionsFunctionHasChanged = true; // reset page so that new options call is used to search\n }\n this._config = value;\n }\n\n get config(): any {\n return this._config;\n }\n\n shouldSearch(value: unknown): boolean {\n const termHasChanged = value !== this._term;\n const optionsNotYetCalled = this.page === 0;\n\n return termHasChanged || optionsNotYetCalled || this.optionsFunctionHasChanged;\n }\n\n addScrollListener(): void {\n if (this.config.enableInfiniteScroll) {\n const element: Element = this.getListElement();\n if (element && !element.hasAttribute('scrollListener')) {\n element.setAttribute('scrollListener', 'true');\n element.addEventListener('scroll', this.scrollHandler);\n }\n }\n }\n\n processSearch(shouldReset?: boolean) {\n this.hasError = false;\n this.isLoading = true;\n this.ref.markForCheck();\n this.search(this.term).subscribe(\n (results: any) => {\n if (shouldReset) {\n this.matches = [];\n }\n if (this.isStatic) {\n this.matches = this.filterData(results);\n } else {\n this.matches = this.matches.concat(results);\n this.lastPage = results && !results.length;\n }\n if (this.matches.length > 0 && this.autoSelectFirstOption && !this.selectingMatches) {\n this.nextActiveMatch();\n }\n this.isLoading = false;\n this.ref.markForCheck();\n setTimeout(() => {\n this.overlay.updatePosition();\n this.addScrollListener();\n });\n },\n (err) => {\n this.hasError = this.term && this.term.length !== 0;\n this.isLoading = false;\n this.lastPage = true;\n if (this.term && this.term.length !== 0) {\n console.error(err);\n }\n this.ref.markForCheck();\n },\n );\n }\n\n search(term, mode?): Observable<any> {\n const options = this.config.options;\n return from(\n new Promise((resolve, reject) => {\n // Check if there is match data\n if (options) {\n // Resolve the data\n if (Array.isArray(options)) {\n this.isStatic = true;\n // Arrays are returned immediately\n resolve(this.structureArray(options));\n } else if (this.shouldCallOptionsFunction(term)) {\n if (\n (options.hasOwnProperty('reject') && options.hasOwnProperty('resolve')) ||\n Object.getPrototypeOf(options).hasOwnProperty('then')\n ) {\n this.isStatic = false;\n // Promises (ES6 or Deferred) are resolved whenever they resolve\n options.then(this.structureArray.bind(this)).then(resolve, reject);\n } else if (typeof options === 'function') {\n this.isStatic = false;\n // Promises (ES6 or Deferred) are resolved whenever they resolve\n options(term, ++this.page)\n .then(this.structureArray.bind(this))\n .then(resolve, reject);\n } else {\n // All other kinds of data are rejected\n reject('The data provided is not an array or a promise');\n throw new Error('The data provided is not an array or a promise');\n }\n } else {\n if (this.config.defaultOptions) {\n this.isStatic = false;\n if (typeof this.config.defaultOptions === 'function') {\n const defaultOptions = this.config.defaultOptions(term, ++this.page);\n if (Object.getPrototypeOf(defaultOptions).hasOwnProperty('then')) {\n defaultOptions.then(this.structureArray.bind(this)).then(resolve, reject);\n } else {\n resolve(this.structureArray(defaultOptions));\n }\n } else {\n resolve(this.structureArray(this.config.defaultOptions));\n }\n } else {\n // No search term gets rejected\n reject('No search term');\n }\n }\n } else {\n // No data gets rejected\n reject('error');\n }\n }),\n );\n }\n\n shouldCallOptionsFunction(term: string): boolean {\n if (this.config && 'minSearchLength' in this.config && Number.isInteger(this.config.minSearchLength)) {\n return typeof term === 'string' && term.length >= this.config.minSearchLength;\n } else {\n return !!(term && term.length);\n }\n }\n\n /**\n * @param collection - the data once getData resolves it\n *\n * @description This function structures an array of nodes into an array of objects with a\n * 'name' field by default.\n */\n structureArray(collection: any): any {\n const dataArray = collection.data ? collection.data : collection;\n if (dataArray && (typeof dataArray[0] === 'string' || typeof dataArray[0] === 'number')) {\n return collection.map((item) => {\n return {\n value: item,\n label: item,\n };\n });\n }\n return dataArray.map((data) => {\n let value = this.config.field ? data[this.config.field] : data.value || data;\n if (this.config.valueFormat) {\n value = Helpers.interpolate(this.config.valueFormat, data);\n }\n const label = this.config.format ? Helpers.interpolate(this.config.format, data) : data.label || String(value);\n return { value, label, data };\n });\n }\n\n /**\n * @param matches - Collection of objects=\n *\n * @description This function loops through the picker options and creates a filtered list of objects that contain\n * the newSearch.\n */\n filterData(matches): Array<any> {\n if (this.term && matches) {\n return matches.filter((match) => {\n return ~String(match.label).toLowerCase().indexOf(this.term.toLowerCase());\n });\n }\n // Show no recent results template\n return matches;\n }\n\n /**\n * @description This function is called when the user presses the enter key to call the selectMatch method.\n */\n selectActiveMatch() {\n this.selectMatch();\n }\n\n /**\n * @description This function sets activeMatch to the match before the current node.\n */\n prevActiveMatch() {\n const index = this.matches.indexOf(this.activeMatch);\n this.activeMatch = this.matches[index - 1 < 0 ? this.matches.length - 1 : index - 1];\n this.scrollToActive();\n this.ref.markForCheck();\n }\n\n /**\n * @description This function sets activeMatch to the match after the current node.\n */\n nextActiveMatch() {\n const index = this.matches.indexOf(this.activeMatch);\n this.activeMatch = this.matches[index + 1 > this.matches.length - 1 ? 0 : index + 1];\n this.scrollToActive();\n this.ref.markForCheck();\n }\n\n getListElement() {\n return this.element.nativeElement;\n }\n\n getChildrenOfListElement() {\n let children = [];\n if (this.getListElement()) {\n children = this.getListElement().children;\n }\n return children;\n }\n\n scrollToActive() {\n const list = this.getListElement();\n const items = this.getChildrenOfListElement();\n const index = this.matches.indexOf(this.activeMatch);\n const item = items[index];\n if (item) {\n list.scrollTop = item.offsetTop;\n }\n }\n\n /**\n * @description\n */\n selectActive(match) {\n this.activeMatch = match;\n }\n\n /**\n * @description\n */\n isActive(match) {\n return this.activeMatch === match;\n }\n\n /**\n * @description\n */\n selectMatch(event?: any, item?: any) {\n if (event) {\n event.stopPropagation();\n event.preventDefault();\n }\n\n const selected = this.activeMatch;\n if (selected && this.parent) {\n this.parent.value = selected;\n this.selectingMatches = true;\n if (this.parent.closeOnSelect) {\n this.parent.hideResults();\n }\n this.selectingMatches = false;\n }\n this.ref.markForCheck();\n return false;\n }\n\n /**\n * @description This function captures the whole query string and replace it with the string that will be used to\n * match.\n */\n escapeRegexp(queryToEscape) {\n // Ex: if the capture is \"a\" the result will be \\a\n return queryToEscape.replace(/([.?*+^$[\\]\\\\(){}|-])/g, '\\\\$1');\n }\n\n /**\n * @deprecated use highlight pipe\n */\n highlight(match, query) {\n // Replaces the capture string with a the same string inside of a \"strong\" tag\n return query ? match.replace(new RegExp(this.escapeRegexp(query.trim()), 'gi'), '<strong>$&</strong>') : match;\n }\n\n preselected(match) {\n let selected = this.selected;\n if (this.config && this.config.selected) {\n selected = [...this.selected, ...this.config.selected];\n }\n if (this.config && this.config.preselected) {\n const preselectedFunc: Function = this.config.preselected;\n return (\n selected.findIndex((item) => {\n return preselectedFunc(match, item);\n }) !== -1\n );\n }\n return (\n selected.findIndex((item) => {\n let isPreselected = false;\n if (item && item.value && match && match.value) {\n if (item.value.id && match.value.id) {\n isPreselected = item.value.id === match.value.id;\n } else if (item.value instanceof Object && item.value.hasOwnProperty('value')) {\n isPreselected = item.value.value === match.value;\n } else {\n isPreselected = item.value === match.value;\n }\n }\n return isPreselected;\n }) !== -1\n );\n }\n}\n","// NG2\nimport { ChangeDetectorRef, Component, ElementRef } from '@angular/core';\n// Vendor\nimport { from, Observable } from 'rxjs';\nimport { NovoLabelService } from 'novo-elements/services';\nimport { Helpers } from 'novo-elements/utils';\n// APP\nimport { BasePickerResults } from '../base-picker-results/BasePickerResults';\n\n/**\n * @description This is the actual list of matches that gets injected into the DOM.\n */\n@Component({\n selector: 'checklist-picker-results',\n host: {\n class: 'active picker-results',\n },\n template: `\n <novo-loading theme=\"line\" *ngIf=\"isLoading && !matches.length\"></novo-loading>\n <ul *ngIf=\"matches.length > 0\">\n <span *ngFor=\"let section of matches; let i = index\">\n <li class=\"header caption\" *ngIf=\"section.data.length > 0\">{{ section.label || section.type }}</li>\n <li\n *ngFor=\"let match of section.data; let i = index\"\n [ngClass]=\"{ checked: match.checked }\"\n (click)=\"selectMatch($event, match)\"\n [class.active]=\"match === activeMatch\"\n (mouseenter)=\"selectActive(match)\"\n >\n <label>\n <i\n [ngClass]=\"{\n 'bhi-checkbox-empty': !match.checked,\n 'bhi-checkbox-filled': match.checked,\n 'bhi-checkbox-indeterminate': match.indeterminate\n }\"\n ></i>\n {{ match.label }}\n </label>\n </li>\n </span>\n </ul>\n <p class=\"picker-error\" *ngIf=\"hasError\">{{ labels.pickerError }}</p>\n <p class=\"picker-null-results\" *ngIf=\"!isLoading && !matches.length && !hasError && term !== ''\">{{ labels.pickerEmpty }}</p>\n `,\n standalone: false,\n})\nexport class ChecklistPickerResults extends BasePickerResults {\n filteredMatches: any;\n\n constructor(element: ElementRef, public labels: NovoLabelService, ref: ChangeDetectorRef) {\n super(element, ref);\n }\n\n search(): Observable<any> {\n const options = this.config.options;\n // only set this the first time\n return from(\n new Promise((resolve, reject) => {\n // Check if there is match data\n if (options) {\n // Resolve the data\n if (Array.isArray(options)) {\n this.isStatic = true;\n // Arrays are returned immediately\n resolve(options);\n } else {\n // All other kinds of data are rejected\n reject('The data provided is not an array or a promise');\n throw new Error('The data provided is not an array or a promise');\n }\n } else {\n // No data gets rejected\n reject('error');\n }\n }),\n );\n }\n\n /**\n * @param matches - Collection of objects=\n *\n * @description This function loops through the picker options and creates a filtered list of objects that contain\n * the newSearch.\n */\n filterData(matches): any {\n if (this.term && matches) {\n this.filteredMatches = matches.map((section) => {\n const items = section.originalData.filter((match) => {\n return ~String(match.label).toLowerCase().indexOf(this.term.toLowerCase());\n });\n section.data = items;\n return section;\n }, this);\n return this.filteredMatches;\n } else if (this.term === '') {\n matches.forEach((section) => {\n section.data = section.originalData;\n });\n return matches;\n }\n // Show no recent results template\n return matches;\n }\n\n selectMatch(event, item) {\n Helpers.swallowEvent(event);\n if (item.indeterminate) {\n item.indeterminate = false;\n item.checked = true;\n } else {\n item.checked = !item.checked;\n }\n\n const selected = this.activeMatch;\n if (selected) {\n this.parent.value = selected;\n }\n this.ref.markForCheck();\n return false;\n }\n}\n","// NG2\nimport { ChangeDetectorRef, Component, ElementRef, HostBinding } from '@angular/core';\nimport { DomSanitizer } from '@angular/platform-browser';\nimport { NovoLabelService } from 'novo-elements/services';\n// Vendor\nimport { BasePickerResults } from '../base-picker-results/BasePickerResults';\n\n@Component({\n selector: 'distribution-list-picker-results',\n template: `\n <section class=\"picker-loading\" *ngIf=\"isLoading && !matches?.length\">\n <novo-loading theme=\"line\"></novo-loading>\n </section>\n <novo-list direction=\"vertical\" *ngIf=\"matches?.length > 0 && !hasError\">\n <novo-list-item\n *ngFor=\"let match of matches\"\n (click)=\"selectMatch($event)\"\n [class.active]=\"match === activeMatch\"\n (mouseenter)=\"selectActive(match)\"\n [class.disabled]=\"preselected(match)\"\n >\n <item-header>\n <item-title>\n <span [innerHtml]=\"sanitizeHTML(match.label)\"></span>\n </item-title>\n </item-header>\n <item-content direction=\"horizontal\">\n <p>\n <span class=\"label\">{{ labels.distributionListOwner }}: </span><span>{{ match?.data?.owner?.name }}</span>\n </p>\n <p>\n <span class=\"label\">{{ labels.dateAdded }}: </span\n ><span>{{ labels.formatDateWithFormat(match?.data?.dateAdded, { year: 'numeric', month: 'numeric', day: 'numeric' }) }}</span>\n </p>\n </item-content>\n </novo-list-item>\n <novo-loading theme=\"line\" *ngIf=\"isLoading && matches?.length > 0\"></novo-loading>\n </novo-list>\n `,\n standalone: false,\n})\nexport class DistributionListPickerResults extends BasePickerResults {\n @HostBinding('class.active')\n active: boolean = true;\n @HostBinding('hidden')\n get isHidden(): boolean {\n return this.matches.length === 0;\n }\n\n constructor(element: ElementRef, private sanitizer: DomSanitizer, public labels: NovoLabelService, ref: ChangeDetectorRef) {\n super(element, ref);\n this.sanitizer = sanitizer;\n }\n\n getListElement(): any {\n return this.element.nativeElement.querySelector('novo-list');\n }\n\n sanitizeHTML(html: any): any {\n return this.sanitizer.bypassSecurityTrustHtml(html);\n }\n}\n","import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';\nimport { NovoLabelService } from 'novo-elements/services';\nimport { BasePickerResults } from '../base-picker-results/BasePickerResults';\n\n@Component({\n selector: 'entity-picker-result',\n template: `\n <novo-list-item *ngIf=\"match.data\" (click)=\"select.next(match.data)\">\n <novo-item-header>\n <novo-item-avatar [icon]=\"getIconForResult(match.data)\"></novo-item-avatar>\n <novo-item-title> <span [innerHtml]=\"getNameForResult(match.data) | highlight:term\"></span> </novo-item-title>\n </novo-item-header>\n <novo-item-content direction=\"horizontal\">\n <!-- COMPANY 1 -->\n <novo-text smaller class=\"company\" *ngIf=\"match.data.companyName || match.data?.clientCorporation?.name\">\n <i class=\"bhi-company company\"></i>\n <span [innerHtml]=\"match.data.companyName || match.data?.clientCorporation?.name | highlight:term\"></span>\n </novo-text>\n <!-- CLIENT CONTACT -->\n <novo-text smaller class=\"contact\" *ngIf=\"match.data?.clientContact?.firstName\">\n <i class=\"bhi-person contact person\"></i>\n <span [innerHtml]=\"match.data.clientContact.firstName + ' ' + match.data.clientContact.lastName | highlight:term\"></span>\n </novo-text>\n <!-- CANDIDATE -->\n <novo-text smaller class=\"candidate\" *ngIf=\"match.data.candidate && match.data.searchEntity === 'Placement'\">\n <i class=\"bhi-candidate candidate\"></i>\n <span [innerHtml]=\"match.data.candidate.firstName + ' ' + match.data.candidate.lastName | highlight:term\"></span>\n </novo-text>\n <!-- START & END DATE -->\n <novo-text smaller class=\"start-date\" *ngIf=\"match.data.dateBegin && match.data.searchEntity === 'Placement'\">\n <i class=\"bhi-calendar\"></i>\n <span [innerHtml]=\"renderTimestamp(match.data.dateBegin) + ' - ' + renderTimestamp(match.data.dateEnd)\"></span>\n </novo-text>\n <!-- START Date -->\n <novo-text smaller class=\"start-date\" *ngIf=\"match.data.startTime && match.data.searchEntity === 'JobShift'\">\n <i class=\"bhi-calendar\"></i>\n <span [innerHtml]=\"renderTimestamp(match.data.startTime)\"></span>\n </novo-text>\n <!-- START & END TIME -->\n <novo-text smaller class=\"start-time\" *ngIf=\"match.data.startTime && match.data.searchEntity === 'JobShift'\">\n <i class=\"bhi-clock\"></i>\n <span [innerHtml]=\"renderTimeNoOffset(match.data.startTime) + ' - ' + renderTimeNoOffset(match.data.endTime)\"></span>\n </novo-text>\n <!-- JOBORDER -->\n <novo-text smaller class=\"job\" *ngIf=\"match.data.jobOrder && match.data.searchEntity === 'JobShift'\">\n <i class=\"bhi-job job\"></i>\n <span [innerHtml]=\"match.data.jobOrder.title | highlight:term\"></span>\n </novo-text>\n <!-- OPENINGS -->\n <novo-text smaller class=\"openings\" *ngIf=\"match.data.openings && match.data.searchEntity === 'JobShift'\">\n <i class=\"bhi-candidate\"></i>\n <span>{{ match.data.numAssigned }} / {{ match.data.openings }}</span>\n </novo-text>\n <!-- EMAIL -->\n <novo-text smaller class=\"email\" *ngIf=\"match.data.email\">\n <i class=\"bhi-email\"></i> <span [innerHtml]=\"match.data.email | highlight:term\"></span>\n </novo-text>\n <!-- PHONE -->\n <novo-text smaller class=\"phone\" *ngIf=\"match.data.phone\">\n <i class=\"bhi-phone\"></i> <span [innerHtml]=\"match.data.phone | highlight:term\"></span>\n </novo-text>\n <!-- ADDRESS -->\n <novo-text smaller class=\"location\" *ngIf=\"match.data.address && (match.data.address.city || match.data.address.state)\">\n <i class=\"bhi-location\"></i> <span *ngIf=\"match.data.address.city\" [innerHtml]=\"highlight(match.data.address.city, term)\"></span>\n <span *ngIf=\"match.data.address.city && match.data.address.state\">, </span>\n <span *ngIf=\"match.data.address.state\" [innerHtml]=\"match.data.address.state | highlight:term\"></span>\n </novo-text>\n <!-- STATUS -->\n <novo-text smaller class=\"status\" *ngIf=\"match.data.status\">\n <i class=\"bhi-info\"></i> <span [innerHtml]=\"match.data.status | highlight:term\"></span>\n </novo-text>\n <!-- OWNER -->\n <novo-text smaller class=\"owner\" *ngIf=\"match.data.owner && match.data.owner.name && match.data.searchEntity === 'Candidate'\">\n <i class=\"bhi-person\"></i> <span [innerHtml]=\"match.data.owner.name | highlight:term\"></span>\n </novo-text>\n <!-- PRIMARY DEPARTMENT -->\n <novo-text\n smaller\n class=\"primary-department\"\n *ngIf=\"match.data.primaryDepartment && match.data.primaryDepartment.name && match.data.searchEntity === 'CorporateUser'\"\n >\n <i class=\"bhi-department\"></i> <span [innerHtml]=\"match.data.primaryDepartment.name | highlight:term\"></span>\n </novo-text>\n <!-- OCCUPATION -->\n <novo-text smaller class=\"occupation\" *ngIf=\"match.data.occupation && match.data.searchEntity === 'CorporateUser'\">\n <i class=\"bhi-occupation\"></i> <span [innerHtml]=\"match.data.occupation | highlight:term\"></span>\n </novo-text>\n </novo-item-content>\n </novo-list-item>\n `,\n styleUrls: ['../picker-results/PickerResult.scss'],\n standalone: false,\n})\nexport class EntityPickerResult {\n @Input() match: any;\n @Input() term: any;\n @Output() select: EventEmitter<any> = new EventEmitter();\n\n constructor(public labels: NovoLabelService) {}\n\n /**\n * @description This function captures the whole query string and replace it with the string that will be used to\n * match.\n */\n escapeRegexp(queryToEscape) {\n // Ex: if the capture is \"a\" the result will be \\a\n return queryToEscape.replace(/([.?*+^$[\\]\\\\(){}|-])/g, '\\\\$1');\n }\n\n /**\n * @deprecated use highlight pipe\n */\n highlight(match, query) {\n // Replaces the capture string with a the same string inside of a \"strong\" tag\n return query && match ? match.replace(new RegExp(this.escapeRegexp(query.trim()), 'gi'), '<strong>$&</strong>') : match;\n }\n\n getIconForResult(result?: any): string {\n if (result) {\n switch (result.searchEntity) {\n case 'ClientContact':\n return 'person contact';\n case 'ClientCorporation':\n return 'company';\n case 'Opportunity':\n return 'opportunity';\n case 'Candidate':\n return 'candidate';\n case 'Lead':\n return 'lead';\n case 'JobOrder':\n return 'job';\n case 'Placement':\n return 'star placement';\n case 'CorporateUser':\n return 'user';\n case 'CorporationDepartment':\n return 'department';\n case 'JobShift':\n return 'timetable contract';\n default:\n return '';\n }\n }\n return '';\n }\n\n renderTimestamp(date?: any) {\n let timestamp = '';\n if (date) {\n timestamp = this.labels.formatDateWithFormat(date, { year: 'numeric', month: 'numeric', day: 'numeric' });\n }\n return timestamp;\n }\n\n renderTime(dateStr?: string) {\n let timestamp = '';\n if (dateStr) {\n timestamp = this.labels.formatTime(new Date(dateStr));\n }\n return timestamp;\n }\n\n renderTimeNoOffset(dateStr?: string) {\n let timestamp = '';\n if (dateStr) {\n dateStr = dateStr.slice(0, 19);\n timestamp = this.labels.formatTime(dateStr);\n }\n return timestamp;\n }\n\n getNameForResult(result?: any) {\n if (result) {\n switch (result.searchEntity) {\n case 'Lead':\n case 'CorporateUser':\n case 'ClientContact':\n case 'Candidate':\n case 'Person':\n if ('firstName' in result) {\n return `${result.firstName} ${result.lastName}`.trim();\n }\n return `${result.name || ''}`.trim();\n case 'ClientCorporation':\n return `${result.name || ''}`.trim();\n case 'Opportunity':\n case 'JobOrder':\n case 'BillingProfile':\n case 'InvoiceTerm':\n return `${result.id} | ${result.title || ''}`.trim();\n case 'Placement':\n let label = `${result.id}`;\n if (result.candidate || result.jobOrder) {\n if (result.candidate && result.jobOrder) {\n label = `${label} | ${result.candidate.firstName} ${result.candidate.lastName} - ${result.jobOrder.title}`.trim();\n } else if (result.jobOrder) {\n label = `${label} | ${result.jobOrder.title}`.trim();\n } else {\n label = `${label} | ${result.candidate.firstName} ${result.candidate.lastName}`.trim();\n }\n }\n return label;\n case 'JobShift':\n return `${result.jobOrder?.title} @ ${result.jobOrder?.clientCorporation?.name || ''}`.trim();\n default:\n return `${result.name || result.label || ''}`.trim();\n }\n }\n return '';\n }\n}\n\n@Component({\n selector: 'entity-picker-results',\n template: `\n <novo-list *ngIf=\"matches.length > 0\" direction=\"vertical\">\n <entity-picker-result\n *ngFor=\"let match of matches\"\n [match]=\"match\"\n [term]=\"term\"\n [ngClass]=\"{ active: match === activeMatch }\"\n (click)=\"selectMatch($event, match)\"\n (mouseenter)=\"selectActive(match)\"\n [class.disabled]=\"preselected(match)\"\n >\n </entity-picker-result>\n <novo-loading theme=\"line\" *ngIf=\"isLoading && matches.length > 0\"></novo-loading>\n </novo-list>\n <div class=\"picker-error\" *ngIf=\"hasError\">{{ labels.pickerError }}</div>\n <div class=\"picker-null-results\" *ngIf=\"hasNonErrorMessage && term !== ''\">{{ labels.pickerEmpty }}</div>\n <div class=\"picker-null-results\" *ngIf=\"hasNonErrorMessage && term === ''\">{{ labels.pickerTextFieldEmpty }}</div>\n `,\n styleUrls: ['../picker-results/PickerResults.scss'],\n encapsulation: ViewEncapsulation.None,\n host: {\n class: 'novo-entity-picker-results',\n },\n standalone: false,\n})\nexport class EntityPickerResults extends BasePickerResults {\n @Output() select: EventEmitter<any> = new EventEmitter();\n\n constructor(element: ElementRef, public labels: NovoLabelService, ref: ChangeDetectorRef) {\n super(element, ref);\n }\n\n get hasNonErrorMessage() {\n return !this.isLoading && !this.matches.length && !this.hasError;\n }\n\n getListElement() {\n return this.element.nativeElement.querySelector('novo-list');\n }\n\n selectMatch(event?: any, item?: any) {\n this.select.next(item);\n return super.selectMatch(event, item);\n }\n}\n","import { ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';\nimport { fromEvent, Subscription } from 'rxjs';\nimport { debounceTime, distinctUntilChanged } from 'rxjs/operators';\nimport { NovoLabelService } from 'novo-elements/services';\nimport { Helpers } from 'novo-elements/utils';\nimport { NovoListElement } from 'novo-elements/elements/list';\nimport { BasePickerResults } from '../base-picker-results/BasePickerResults';\n\n@Component({\n selector: 'grouped-multi-picker-results',\n template: `\n <div class=\"grouped-multi-picker-groups\">\n <novo-list direction=\"vertical\">\n <novo-list-item\n *ngIf=\"config.displayAll\"\n (click)=\"selectCategory({ value: 'all', label: 'all' })\"\n [class.active]=\"selectedCategory?.value === 'all'\"\n data-automation-id=\"display-all\"\n [class.disabled]=\"isLoading\"\n >\n <item-content>\n <span data-automation-id=\"label\">{{ labels.all }}</span>\n </item-content>\n <item-end>\n <i class=\"bhi-next\"></i>\n </item-end>\n </novo-list-item>\n <novo-list-item\n *ngFor=\"let category of categories\"\n (click)=\"selectCategory(category)\"\n [class.active]=\"selectedCategory?.value === category.value\"\n [attr.data-automation-id]=\"category.label\"\n [class.disabled]=\"isLoading\"\n >\n <item-content>\n <i *ngIf=\"category.iconClass\" [class]=\"category.iconClass\"></i>\n <span data-automation-id=\"label\">{{ category.label }}</span>\n </item-content>\n <item-end>\n <i class=\"bhi-next\"></i>\n </item-end>\n </novo-list-item>\n </novo-list>\n <footer\n class=\"grouped-multi-picker-groups-footer\"\n *ngIf=\"customFilterEnabled\"\n data-automation-id=\"footer\"\n [class.disabled]=\"isLoading\"\n >\n <novo-switch [(ngModel)]=\"customFilterValue\" (onChange)=\"fireCustomFilter($event)\" data-automation-id=\"switch\"></novo-switch>\n <label data-automation-id=\"label\">{{ customFilterLabel }}</label>\n </footer>\n </div>\n <div class=\"grouped-multi-picker-matches\">\n <div class=\"grouped-multi-picker-input-container\" [hidden]=\"!selectedCategory\" data-automation-id=\"input-container\">\n <input autofocus #input [(ngModel)]=\"searchTerm\" [disabled]=\"isLoading\" data-automation-id=\"input\" [placeholder]=\"placeholder\" />\n <i class=\"bhi-search\" *ngIf=\"!searchTerm\" [class.disabled]=\"isLoading\" data-automation-id=\"seach-icon\"></i>\n <i\n class=\"bhi-times\"\n *ngIf=\"searchTerm\"\n (click)=\"clearSearchTerm($event)\"\n [class.disabled]=\"isLoading\"\n data-automation-id=\"remove-icon\"\n ></i>\n </div>\n <div class=\"grouped-multi-picker-list-container\">\n <novo-list direction=\"vertical\" #list>\n <novo-list-item\n *ngFor=\"let match of matches\"\n (click)=\"selectMatch($event)\"\n [class.active]=\"match === activeMatch\"\n (mouseenter)=\"selectActive(match)\"\n [class.disabled]=\"preselected(match) || isLoading\"\n [attr.data-automation-id]=\"match.label\"\n >\n <item-content>\n <span>{{ match.label }}</span>\n </item-content>\n </novo-list-item>\n </novo-list>\n <div\n class=\"grouped-multi-picker-no-results\"\n *ngIf=\"matches.length === 0 && !isLoading && selectedCategory\"\n data-automation-id=\"empty-message\"\n >\n {{ labels.groupedMultiPickerEmpty }}\n </div>\n <div\n class=\"grouped-multi-picker-no-category\"\n *ngIf=\"matches.length === 0 && !isLoading && !selectedCategory\"\n data-automation-id=\"select-category-message\"\n >\n {{ labels.groupedMultiPickerSelectCategory }}\n </div>\n <div class=\"grouped-multi-picker-loading\" *ngIf=\"isLoading\" data-automation-id=\"loading-message\">\n <novo-loading theme=\"line\"></novo-loading>\n </div>\n </div>\n </div>\n `,\n styleUrls: ['./GroupedMultiPickerResults.scss'],\n standalone: false,\n})\nexport class GroupedMultiPickerResults extends BasePickerResults implements OnInit, OnDestroy {\n @ViewChild('input', { static: true })\n private inputElement: ElementRef;\n @ViewChild('list')\n private listElement: NovoListElement;\n\n public selectedCategory: { value: string; label: string };\n public searchTerm: string;\n public customFilterEnabled: boolean = false;\n public customFilterLabel: string;\n public placeholder: string = '';\n\n private keyboardSubscription: Subscription;\n private internalMap: Map<string, { value: string; label: string; items: { value: string; label: string }[] }> = new Map<\n string,\n { value: string; label: string; items: { value: string; label: string }[] }\n >();\n public customFilterValue: any;\n\n set term(value) {\n // Display all only will work for static categories\n if (this.config.displayAll && this.config.getItemsForCategoryAsync) {\n throw new Error(\n '[GroupedMultiPickerResults] - you can only have `displayAll` with a static `categoryMap`. Not available with `getItemsForCategoryAsync`',\n );\n }\n // Custom filter\n if (this.config.customFilter) {\n this.customFilterEnabled = true;\n this.customFilterLabel = this.config.customFilter.label;\n this.customFilterValue = !!this.config.customFilter.defaultFilterValue;\n this.ref.markForCheck();\n if (!this.customFilterLabel || !this.config.customFilter.matchFunction) {\n throw new Error('[GroupedMultiPickerResults] - custom filter/matchFunction set no label was provided!');\n }\n } else {\n this.customFilterEnabled = false;\n }\n // Configure ALL\n if (this.config.displayAll && !this.selectedCategory) {\n this.setAllCategory();\n }\n // Placeholder\n if (this.config.placeholder) {\n this.placeholder = this.config.placeholder;\n }\n // Focus\n setTimeout(() => {\n this.inputElement.nativeElement.focus();\n });\n }\n\n get categories() {\n if (this.config.categories || this.config.categoryMap) {\n return (\n this.config.categories ||\n Array.from(this.config.categoryMap.values()).filter((category: { value: string }) => {\n return category.value !== 'all';\n })\n );\n }\n return [];\n }\n\n constructor(element: ElementRef, private renderer: Renderer2, public labels: NovoLabelService, ref: ChangeDetectorRef) {\n super(element, ref);\n }\n\n public ngOnInit() {\n // Subscribe to keyboard events and debounce\n this.keyboardSubscription = fromEvent(this.inputElement.nativeElement, 'keyup')\n .pipe(debounceTime(350), distinctUntilChanged())\n .subscribe((event: KeyboardEvent) => {\n this.searchTerm = (event.target as HTMLInputElement).value;\n this.matches = this.filterData();\n this.ref.markForCheck();\n });\n }\n\n public ngOnDestroy() {\n // Cleanup\n this.keyboardSubscription.unsubscribe();\n }\n\n public setAllCategory() {\n // If we have display all, set the all categories up\n if (this.config.displayAll) {\n this.selectedCategory = { value: 'all', label: 'all' };\n const allItems = [];\n Array.from(this.config.categoryMap.values())\n .filter((category: { value: string }) => {\n return category.value !== 'all';\n })\n .forEach((v: { value: string; label: string; items: any[] }) => allItems.push(...v.items));\n this.matches = this.filter(allItems);\n this.config.categoryMap.set('all', { value: 'all', label: 'All', items: allItems });\n this.ref.markForCheck();\n }\n }\n\n public selectCategory(category: { value: string; label: string }): void {\n // Scroll to top\n this.renderer.setProperty(this.listElement.element.nativeElement, 'scrollTop', 0);\n // Set focus\n this.inputElement.nativeElement.focus();\n // Find new items\n const key: string = category.value;\n this.selectedCategory = category;\n // Clear\n this.matches = [];\n this.ref.markForCheck();\n // New matches\n this.getNewMatches(category, key);\n }\n\n public clearSearchTerm(event: MouseEvent) {\n Helpers.swallowEvent(event);\n this.searchTerm = '';\n this.selectCategory({ value: this.selectedCategory.value, label: this.selectedCategory.label });\n this.ref.markForCheck();\n }\n\n public selectMatch(event?: MouseEvent, item?: { value: string; label: string }): boolean {\n // Set focus\n this.inputElement.nativeElement.focus();\n return super.selectMatch(event);\n }\n\n public fireCustomFilter(value: boolean) {\n this.customFilterValue = value;\n // Clear cache map\n this.internalMap.clear();\n // Only fire if we have a selected category\n if (this.selectCategory) {\n // Find new items\n const key: string = this.selectedCategory.value;\n // Get new matches\n this.getNewMatches(this.selectedCategory, key);\n this.ref.markForCheck();\n }\n // Focus\n setTimeout(() => {\n this.inputElement.nativeElement.focus();\n });\n }\n\n filterData(): { value: string; label: string }[] {\n if (this.selectedCategory) {\n if (this.config.categoryMap) {\n return this.filter(this.config.categoryMap.get(this.selectedCategory.value).items);\n } else {\n return this.filter(this.internalMap.get(this.selectedCategory.value).items);\n }\n }\n return [];\n }\n\n private getNewMatches(category: { value: string; label: string }, key: string): void {\n // Get new matches\n if (this.config.categoryMap) {\n this.matches = this.filter(this.config.categoryMap.get(key).items);\n this.ref.markForCheck();\n } else {\n if (!this.config.getItemsForCategoryAsync) {\n throw new Error(\n 'The \"config\" for the Chips must include a function \"getItemsForCategoryAsync(categoryKey: string)\" to retrieve the items by category. Or if you have static data provide a \"categoryMap\"',\n );\n }\n if (!this.internalMap.get(key)) {\n this.isLoading = true;\n this.config.getItemsForCategoryAsync(key, this.customFilterValue).then((items: { value: string; label: string }[]) => {\n this.internalMap.set(key, { value: category.value, label: category.label, items });\n this.matches = this.filter(items, true);\n this.isLoading = false;\n this.ref.markForCheck();\n setTimeout(() => {\n this.inputElement.nativeElement.focus();\n });\n });\n } else {\n this.matches = this.filter(this.internalMap.get(key).items);\n this.ref.markForCheck();\n }\n }\n }\n\n private filter(\n array: { value: string; label: string; filterValue?: any }[],\n ignoreCustomFilter: boolean = false,\n ): { value: string; label: string }[] {\n let matches: { value: string; label: string; filterValue?: any }[] = array;\n if (this.searchTerm && this.searchTerm.length !== 0 && this.selectedCategory) {\n matches = matches.filter((match) => {\n const searchTerm = this.searchTerm.toLowerCase();\n return match.label.toLowerCase().indexOf(searchTerm) > -1 || match.value.toLowerCase().indexOf(searchTerm) > -1;\n });\n }\n if (this.customFilterEnabled && this.config.customFilter.matchFunction && !ignoreCustomFilter) {\n matches = matches.filter((match) => this.config.customFilter.matchFunction(match, this.customFilterValue));\n }\n return matches;\n }\n}\n","import { ChangeDetectorRef, Component, ElementRef, OnDestroy, Renderer2, ViewChild } from '@angular/core';\nimport { fromEvent, Subject, Subscription } from 'rxjs';\nimport { debounceTime, distinctUntilChanged } from 'rxjs/operators';\nimport { NovoLabelService } from 'novo-elements/services';\nimport { Helpers } from 'novo-elements/utils';\nimport { NovoListElement } from 'novo-elements/elements/list';\nimport { BasePickerResults } from '../base-picker-results/BasePickerResults';\n\nexport interface IMixedMultiPickerOption {\n value: string;\n label: string;\n secondaryOptions?: {\n value: string;\n label: string;\n filterValue?: any;\n }[];\n getSecondaryOptionsAsync?(): Promise<{ value: string; label: string }[]>;\n // TODO: Refactor to prevent the need for a behaviorSubject to allow primaryOption's secondaryOptions to be cleared\n // Currently secondaryOptions cannot be cleared via FieldInteraction API and must use a behavior subject - this includes modifyPickerConfig\n clearSecondaryOptions?: Subject<any>;\n showSearchOnSecondaryOptions?: boolean;\n}\n\n@Component({\n selector: 'mixed-multi-picker-results',\n template: ` <div class=\"mixed-multi-picker-groups\">\n <novo-list direction=\"vertical\">\n <novo-list-item\n *ngFor=\"let option of options\"\n (click)=\"selectPrimaryOption(option, $event)\"\n [class.active]=\"selectedPrimaryOption?.value === option.value\"\n [attr.data-automation-id]=\"option.label\"\n [class.disabled]=\"isLoading\"\n >\n <item-content>\n <i *ngIf=\"option.iconClass\" [class]=\"option.iconClass\"></i>\n <span data-automation-id=\"label\">{{ option.label }}</span>\n </item-content>\n <item-end *ngIf=\"optionHasSecondaryOptions(option)\">\n <i class=\"bhi-next\"></i>\n </item-end>\n </novo-list-item>\n </novo-list>\n </div>\n <div class=\"mixed-multi-picker-matches\" [hidden]=\"!optionHasSecondaryOptions(selectedPrimaryOption)\">\n <div\n class=\"mixed-multi-picker-input-container\"\n [hidden]=\"!shouldShowSearchBox(selectedPrimaryOption)\"\n data-automation-id=\"input-container\"\n >\n <input autofocus #input [(ngModel)]=\"searchTerm\" [disabled]=\"isLoading\" data-automation-id=\"input\" [placeholder]=\"placeholder\" />\n <i class=\"bhi-search\" *ngIf=\"!searchTerm\" [class.disabled]=\"isLoading\" data-automation-id=\"seach-icon\"></i>\n <i\n class=\"bhi-times\"\n *ngIf=\"searchTerm\"\n (click)=\"clearSearchTerm($event)\"\n [class.disabled]=\"isLoading\"\n data-automation-id=\"remove-icon\"\n ></i>\n </div>\n <div class=\"mixed-multi-picker-list-container\">\n <novo-list direction=\"vertical\" #list>\n <novo-list-item\n *ngFor=\"let match of matches\"\n (click)=\"selectMatch($event)\"\n [class.active]=\"match === activeMatch\"\n (mouseenter)=\"selectActive(match)\"\n [class.disabled]=\"preselected(match) || isLoading\"\n [attr.data-automation-id]=\"match.label\"\n >\n <item-content>\n <span>{{ match.label }}</span>\n </item-content>\n </novo-list-item>\n </novo-list>\n <div\n class=\"mixed-multi-picker-no-results\"\n *ngIf=\"matches.length === 0 && !isLoading && selectedPrimaryOption\"\n data-automation-id=\"empty-message\"\n >\n {{ config.emptyOptionsLabel ? config.emptyOptionsLabel : labels.groupedMultiPickerEmpty }}\n </div>\n <div class=\"mixed-multi-picker-loading\" *ngIf=\"isLoading\" data-automation-id=\"loading-message\">\n <novo-loading theme=\"line\"></novo-loading>\n </div>\n </div>\n </div>`,\n styleUrls: ['./MixedMultiPickerResults.scss'],\n standalone: false,\n})\nexport class MixedMultiPickerResults extends BasePickerResults implements OnDestroy {\n @ViewChild('input', { static: true })\n private inputElement: ElementRef;\n @ViewChild('list')\n private listElement: NovoListElement;\n\n public selectedPrimaryOption: IMixedMultiPickerOption;\n public searchTerm: string;\n public placeholder: string = '';\n public emptyOptionsLabel: string = '';\n\n private keyboardSubscription: Subscription;\n\n private internalMap: Map<string, { value: string; label: string; items: { value: string; label: string }[] }> = new Map<\n string,\n { value: string; label: string; items: { value: string; label: string }[] }\n >();\n\n set term(value) {\n if (this.config.placeholder) {\n this.placeholder = this.config.placeholder;\n }\n // Focus\n setTimeout(() => {\n this.inputElement.nativeElement.focus();\n });\n }\n\n get options() {\n return this.config.options || [];\n }\n\n constructor(element: ElementRef, private renderer: Renderer2, public labels: NovoLabelService, ref: ChangeDetectorRef) {\n super(element, ref);\n }\n\n public ngOnDestroy() {\n // Cleanup\n if (this.keyboardSubscription) {\n this.keyboardSubscription.unsubscribe();\n }\n if (this.config.options) {\n this.config.options.forEach((option) => {\n if (option.clearSecondaryOptions) {\n option.clearSecondaryOptions.unsubscribe();\n }\n });\n }\n }\n\n public selectPrimaryOption(primaryOption: IMixedMultiPickerOption, event?: MouseEvent): void {\n if (this.keyboardSubscription) {\n this.keyboardSubscription.unsubscribe();\n }\n // Scroll to top\n this.renderer.setProperty(this.listElement.element.nativeElement, 'scrollTop', 0);\n // Set focus\n this.inputElement.nativeElement.focus();\n // Find new items\n const key: string = primaryOption.value;\n this.selectedPrimaryOption = primaryOption;\n // Clear\n this.matches = [];\n this.ref.markForCheck();\n // New matches\n if (this.optionHasSecondaryOptions(primaryOption)) {\n // Subscribe to keyboard events and debounce\n this.keyboardSubscription = fromEvent(this.inputElement.nativeElement, 'keyup')\n .pipe(debounceTime(350), distinctUntilChanged())\n .subscribe((keyEvent: KeyboardEvent) => {\n this.searchTerm = (keyEvent.target as HTMLInputElement).value;\n this.matches = this.filterData();\n this.ref.markForCheck();\n });\n this.getNewMatches(primaryOption);\n } else {\n this.selectActive(primaryOption);\n this.selectMatch(event);\n }\n }\n\n public selectMatch(event?: MouseEvent): boolean {\n // Set focus\n this.inputElement.nativeElement.focus();\n return super.selectMatch(event);\n }\n\n public clearSearchTerm(event: MouseEvent) {\n Helpers.swallowEvent(event);\n this.searchTerm = '';\n this.selectPrimaryOption({ value: this.selectedPrimaryOption.value, label: this.selectedPrimaryOption.label });\n this