(layouts/common/search) Improved the autocomplete design

This commit is contained in:
sercan 2021-06-10 14:56:00 +03:00
parent 39650d3cc9
commit c1c9904b9d
3 changed files with 161 additions and 102 deletions

View File

@ -6,7 +6,6 @@
(click)="open()"> (click)="open()">
<mat-icon [svgIcon]="'heroicons_outline:search'"></mat-icon> <mat-icon [svgIcon]="'heroicons_outline:search'"></mat-icon>
</button> </button>
<div <div
class="absolute inset-0 flex items-center flex-shrink-0 z-99 bg-card" class="absolute inset-0 flex items-center flex-shrink-0 z-99 bg-card"
*ngIf="opened" *ngIf="opened"
@ -23,22 +22,36 @@
(keydown)="onKeydown($event)" (keydown)="onKeydown($event)"
#barSearchInput> #barSearchInput>
<mat-autocomplete <mat-autocomplete
class="max-h-128 border-t rounded-b shadow-md" class="max-h-128 sm:px-2 border-t rounded-b shadow-md"
[disableRipple]="true" [disableRipple]="true"
#matAutocomplete="matAutocomplete"> #matAutocomplete="matAutocomplete">
<mat-option <mat-option
class="h-14 px-6 py-0 sm:px-8 text-md pointer-events-none text-secondary bg-transparent" class="py-0 px-6 text-md pointer-events-none text-secondary bg-transparent"
*ngIf="results && !results.length"> *ngIf="resultSets && !resultSets.length">
No results found! No results found!
</mat-option> </mat-option>
<ng-container *ngFor="let result of results; trackBy: trackByFn"> <ng-container *ngFor="let resultSet of resultSets; trackBy: trackByFn">
<mat-option <mat-optgroup class="flex items-center mt-2 px-2">
class="group relative h-14 px-6 py-0 sm:px-8 text-md" <span class="text-sm font-semibold tracking-wider text-secondary">{{resultSet.label.toUpperCase()}}</span>
[routerLink]="result.link"> </mat-optgroup>
<ng-container <ng-container *ngFor="let result of resultSet.results; trackBy: trackByFn">
[ngTemplateOutlet]="searchResult" <mat-option
[ngTemplateOutletContext]="{$implicit: result}"></ng-container> class="group relative mb-1 py-0 px-6 text-md rounded-md hover:bg-gray-100 dark:hover:bg-hover"
</mat-option> [routerLink]="result.link">
<!-- Contacts -->
<ng-container *ngIf="resultSet.id === 'contacts'">
<ng-container *ngTemplateOutlet="contactResult; context: {$implicit: result}"></ng-container>
</ng-container>
<!-- Pages -->
<ng-container *ngIf="resultSet.id === 'pages'">
<ng-container *ngTemplateOutlet="pageResult; context: {$implicit: result}"></ng-container>
</ng-container>
<!-- Tasks -->
<ng-container *ngIf="resultSet.id === 'tasks'">
<ng-container *ngTemplateOutlet="taskResult; context: {$implicit: result}"></ng-container>
</ng-container>
</mat-option>
</ng-container>
</ng-container> </ng-container>
</mat-autocomplete> </mat-autocomplete>
<button <button
@ -69,62 +82,89 @@
[disableRipple]="true" [disableRipple]="true"
#matAutocomplete="matAutocomplete"> #matAutocomplete="matAutocomplete">
<mat-option <mat-option
class="h-14 px-5 py-0 text-md pointer-events-none text-secondary bg-transparent" class="py-0 px-6 text-md pointer-events-none text-secondary bg-transparent"
*ngIf="results && !results.length"> *ngIf="resultSets && !resultSets.length">
No results found! No results found!
</mat-option> </mat-option>
<ng-container *ngFor="let result of results; trackBy: trackByFn"> <ng-container *ngFor="let resultSet of resultSets; trackBy: trackByFn">
<mat-option <mat-optgroup class="flex items-center mt-2 px-2">
class="group relative h-14 px-5 py-0 text-md" <span class="text-sm font-semibold tracking-wider text-secondary">{{resultSet.label.toUpperCase()}}</span>
[routerLink]="result.link"> </mat-optgroup>
<ng-container <ng-container *ngFor="let result of resultSet.results; trackBy: trackByFn">
[ngTemplateOutlet]="searchResult" <mat-option
[ngTemplateOutletContext]="{$implicit: result}"></ng-container> class="group relative mb-1 py-0 px-6 text-md rounded-md hover:bg-gray-100 dark:hover:bg-hover"
</mat-option> [routerLink]="result.link">
<!-- Contacts -->
<ng-container *ngIf="resultSet.id === 'contacts'">
<ng-container *ngTemplateOutlet="contactResult; context: {$implicit: result}"></ng-container>
</ng-container>
<!-- Pages -->
<ng-container *ngIf="resultSet.id === 'pages'">
<ng-container *ngTemplateOutlet="pageResult; context: {$implicit: result}"></ng-container>
</ng-container>
<!-- Tasks -->
<ng-container *ngIf="resultSet.id === 'tasks'">
<ng-container *ngTemplateOutlet="taskResult; context: {$implicit: result}"></ng-container>
</ng-container>
</mat-option>
</ng-container>
</ng-container> </ng-container>
</mat-autocomplete> </mat-autocomplete>
</div> </div>
</ng-container> </ng-container>
<!-- Contact result template -->
<ng-template <ng-template
#searchResult #contactResult
let-result> let-result>
<div class="flex items-center">
<!-- Hover indicator --> <div class="flex flex-shrink-0 items-center justify-center w-8 h-8 rounded-full overflow-hidden bg-primary-100 dark:bg-primary-800">
<div class="absolute inset-y-0 left-0 hidden w-1 bg-primary group-hover:flex"></div> <img
*ngIf="result.avatar"
<!-- Contact result --> [src]="result.avatar">
<ng-container *ngIf="result.resultType === 'contact'"> <mat-icon
<div class="flex items-center"> class="m-0 icon-size-5 text-primary dark:text-primary-400"
<div class="px-1.5 py-1 mr-4 text-xs font-semibold leading-normal rounded text-indigo-50 bg-indigo-600">Contact</div> *ngIf="!result.avatar"
<div class="overflow-hidden overflow-ellipsis"> [svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
<span [innerHTML]="result.title"></span>
</div>
<div class="flex flex-shrink-0 items-center justify-center w-8 h-8 ml-auto rounded-full overflow-hidden bg-primary-100 dark:bg-black dark:bg-opacity-5">
<img
*ngIf="result.avatar"
[src]="result.avatar">
<mat-icon
class="m-0 icon-size-5 text-primary"
*ngIf="!result.avatar"
[svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
</div>
</div> </div>
</ng-container> <div class="ml-3 truncate">
<span [innerHTML]="result.name"></span>
<!-- Page result -->
<ng-container *ngIf="result.resultType === 'page'">
<div class="flex items-center">
<div class="px-1.5 py-1 mr-4 text-xs font-semibold leading-normal rounded text-teal-50 bg-teal-600">Page</div>
<div class="flex flex-col overflow-hidden overflow-ellipsis">
<span
class="overflow-hidden overflow-ellipsis whitespace-nowrap leading-normal"
[innerHTML]="result.title"></span>
<span
class="mt-1 text-secondary overflow-hidden overflow-ellipsis whitespace-nowrap leading-normal text-sm no-underline"
[routerLink]="result.link">{{result.link}}</span>
</div>
</div> </div>
</ng-container> </div>
</ng-template>
<!-- Page result template -->
<ng-template
#pageResult
let-result>
<div class="flex flex-col">
<div
class="truncate leading-normal"
[innerHTML]="result.title"></div>
<div class="truncate leading-normal text-sm text-secondary">
{{result.link}}
</div>
</div>
</ng-template>
<!-- Task result template -->
<ng-template
#taskResult
let-result>
<div class="flex items-center">
<ng-container *ngIf="result.completed">
<mat-icon
class="mr-0 text-primary dark:text-primary-400"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
</ng-container>
<ng-container *ngIf="!result.completed">
<mat-icon
class="mr-0 text-hint"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
</ng-container>
<div
class="ml-3 truncate leading-normal"
[ngClass]="{'line-through text-hint': result.completed}"
[innerHTML]="result.title"></div>
</div>
</ng-template> </ng-template>

View File

@ -20,7 +20,7 @@ export class SearchComponent implements OnChanges, OnInit, OnDestroy
@Output() search: EventEmitter<any> = new EventEmitter<any>(); @Output() search: EventEmitter<any> = new EventEmitter<any>();
opened: boolean = false; opened: boolean = false;
results: any[]; resultSets: any[];
searchControl: FormControl = new FormControl(); searchControl: FormControl = new FormControl();
private _unsubscribeAll: Subject<any> = new Subject<any>(); private _unsubscribeAll: Subject<any> = new Subject<any>();
@ -104,12 +104,12 @@ export class SearchComponent implements OnChanges, OnInit, OnDestroy
takeUntil(this._unsubscribeAll), takeUntil(this._unsubscribeAll),
map((value) => { map((value) => {
// Set the search results to null if there is no value or // Set the resultSets to null if there is no value or
// the length of the value is smaller than the minLength // the length of the value is smaller than the minLength
// so the autocomplete panel can be closed // so the autocomplete panel can be closed
if ( !value || value.length < this.minLength ) if ( !value || value.length < this.minLength )
{ {
this.results = null; this.resultSets = null;
} }
// Continue // Continue
@ -121,13 +121,13 @@ export class SearchComponent implements OnChanges, OnInit, OnDestroy
) )
.subscribe((value) => { .subscribe((value) => {
this._httpClient.post('api/common/search', {query: value}) this._httpClient.post('api/common/search', {query: value})
.subscribe((response: any) => { .subscribe((resultSets: any) => {
// Store the results // Store the result sets
this.results = response.results; this.resultSets = resultSets;
// Execute the event // Execute the event
this.search.next(this.results); this.search.next(resultSets);
}); });
}); });
} }

View File

@ -4,6 +4,7 @@ import { FuseNavigationItem, FuseNavigationService } from '@fuse/components/navi
import { FuseMockApiService } from '@fuse/lib/mock-api'; import { FuseMockApiService } from '@fuse/lib/mock-api';
import { defaultNavigation } from 'app/mock-api/common/navigation/data'; import { defaultNavigation } from 'app/mock-api/common/navigation/data';
import { contacts } from 'app/mock-api/apps/contacts/data'; import { contacts } from 'app/mock-api/apps/contacts/data';
import { tasks } from 'app/mock-api/apps/tasks/data';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -12,6 +13,7 @@ export class SearchMockApi
{ {
private readonly _defaultNavigation: FuseNavigationItem[] = defaultNavigation; private readonly _defaultNavigation: FuseNavigationItem[] = defaultNavigation;
private readonly _contacts: any[] = contacts; private readonly _contacts: any[] = contacts;
private readonly _tasks: any[] = tasks;
/** /**
* Constructor * Constructor
@ -54,58 +56,75 @@ export class SearchMockApi
return [200, {results: []}]; return [200, {results: []}];
} }
// Filter the navigation
const navigationResults = cloneDeep(flatNavigation).filter(item => (item.title?.toLowerCase().includes(query) || (item.subtitle && item.subtitle.includes(query))));
// Filter the contacts // Filter the contacts
const contactsResults = cloneDeep(this._contacts).filter(user => user.name.toLowerCase().includes(query)); const contactsResults = cloneDeep(this._contacts)
.filter(contact => contact.name.toLowerCase().includes(query));
// Create the results array // Filter the navigation
const pagesResults = cloneDeep(flatNavigation)
.filter(page => (page.title?.toLowerCase().includes(query) || (page.subtitle && page.subtitle.includes(query))));
// Filter the tasks
const tasksResults = cloneDeep(this._tasks)
.filter(task => task.title.toLowerCase().includes(query));
// Prepare the results array
const results = []; const results = [];
// If there are navigation results...
if ( navigationResults.length > 0 )
{
// Normalize the results while marking the found chars
navigationResults.forEach((result: any) => {
// Normalize
result['hint'] = result.link;
result['resultType'] = 'page';
// Mark the found chars
const re = new RegExp('(' + query.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + ')', 'ig');
result.title = result.title.replace(re, '<mark>$1</mark>');
});
// Add the results
results.push(...navigationResults);
}
// If there are contacts results... // If there are contacts results...
if ( contactsResults.length > 0 ) if ( contactsResults.length > 0 )
{ {
// Normalize the results while marking the found chars // Normalize the results
contactsResults.forEach((result) => { contactsResults.forEach((result) => {
// Normalize
result.title = result.name;
result.resultType = 'contact';
// Make the found chars bold
const re = new RegExp('(' + query.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + ')', 'ig');
result.title = result.title.replace(re, '<mark>$1</mark>');
// Add a link // Add a link
result.link = '/apps/contacts/' + result.id; result.link = '/apps/contacts/' + result.id;
}); });
// Add the results to the results object // Add to the results
results.push(...contactsResults); results.push({
id : 'contacts',
label : 'Contacts',
results: contactsResults
});
}
// If there are page results...
if ( pagesResults.length > 0 )
{
// Normalize the results
pagesResults.forEach((result: any) => {
});
// Add to the results
results.push({
id : 'pages',
label : 'Pages',
results: pagesResults
});
}
// If there are tasks results...
if ( tasksResults.length > 0 )
{
// Normalize the results
tasksResults.forEach((result) => {
// Add a link
result.link = '/apps/tasks/' + result.id;
});
// Add to the results
results.push({
id : 'tasks',
label : 'Tasks',
results: tasksResults
});
} }
// Return the response // Return the response
return [200, {results}]; return [200, results];
}); });
} }
} }