(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()">
<mat-icon [svgIcon]="'heroicons_outline:search'"></mat-icon>
</button>
<div
class="absolute inset-0 flex items-center flex-shrink-0 z-99 bg-card"
*ngIf="opened"
@ -23,23 +22,37 @@
(keydown)="onKeydown($event)"
#barSearchInput>
<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"
#matAutocomplete="matAutocomplete">
<mat-option
class="h-14 px-6 py-0 sm:px-8 text-md pointer-events-none text-secondary bg-transparent"
*ngIf="results && !results.length">
class="py-0 px-6 text-md pointer-events-none text-secondary bg-transparent"
*ngIf="resultSets && !resultSets.length">
No results found!
</mat-option>
<ng-container *ngFor="let result of results; trackBy: trackByFn">
<ng-container *ngFor="let resultSet of resultSets; trackBy: trackByFn">
<mat-optgroup class="flex items-center mt-2 px-2">
<span class="text-sm font-semibold tracking-wider text-secondary">{{resultSet.label.toUpperCase()}}</span>
</mat-optgroup>
<ng-container *ngFor="let result of resultSet.results; trackBy: trackByFn">
<mat-option
class="group relative h-14 px-6 py-0 sm:px-8 text-md"
class="group relative mb-1 py-0 px-6 text-md rounded-md hover:bg-gray-100 dark:hover:bg-hover"
[routerLink]="result.link">
<ng-container
[ngTemplateOutlet]="searchResult"
[ngTemplateOutletContext]="{$implicit: result}"></ng-container>
<!-- 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>
</mat-autocomplete>
<button
class="absolute top-1/2 right-5 sm:right-7 flex-shrink-0 w-10 h-10 -mt-5"
@ -69,62 +82,89 @@
[disableRipple]="true"
#matAutocomplete="matAutocomplete">
<mat-option
class="h-14 px-5 py-0 text-md pointer-events-none text-secondary bg-transparent"
*ngIf="results && !results.length">
class="py-0 px-6 text-md pointer-events-none text-secondary bg-transparent"
*ngIf="resultSets && !resultSets.length">
No results found!
</mat-option>
<ng-container *ngFor="let result of results; trackBy: trackByFn">
<ng-container *ngFor="let resultSet of resultSets; trackBy: trackByFn">
<mat-optgroup class="flex items-center mt-2 px-2">
<span class="text-sm font-semibold tracking-wider text-secondary">{{resultSet.label.toUpperCase()}}</span>
</mat-optgroup>
<ng-container *ngFor="let result of resultSet.results; trackBy: trackByFn">
<mat-option
class="group relative h-14 px-5 py-0 text-md"
class="group relative mb-1 py-0 px-6 text-md rounded-md hover:bg-gray-100 dark:hover:bg-hover"
[routerLink]="result.link">
<ng-container
[ngTemplateOutlet]="searchResult"
[ngTemplateOutletContext]="{$implicit: result}"></ng-container>
<!-- 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>
</mat-autocomplete>
</div>
</ng-container>
<!-- Contact result template -->
<ng-template
#searchResult
#contactResult
let-result>
<!-- Hover indicator -->
<div class="absolute inset-y-0 left-0 hidden w-1 bg-primary group-hover:flex"></div>
<!-- Contact result -->
<ng-container *ngIf="result.resultType === 'contact'">
<div class="flex items-center">
<div class="px-1.5 py-1 mr-4 text-xs font-semibold leading-normal rounded text-indigo-50 bg-indigo-600">Contact</div>
<div class="overflow-hidden overflow-ellipsis">
<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">
<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">
<img
*ngIf="result.avatar"
[src]="result.avatar">
<mat-icon
class="m-0 icon-size-5 text-primary"
class="m-0 icon-size-5 text-primary dark:text-primary-400"
*ngIf="!result.avatar"
[svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
</div>
</div>
</ng-container>
<!-- 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 class="ml-3 truncate">
<span [innerHTML]="result.name"></span>
</div>
</div>
</ng-container>
</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>

View File

@ -20,7 +20,7 @@ export class SearchComponent implements OnChanges, OnInit, OnDestroy
@Output() search: EventEmitter<any> = new EventEmitter<any>();
opened: boolean = false;
results: any[];
resultSets: any[];
searchControl: FormControl = new FormControl();
private _unsubscribeAll: Subject<any> = new Subject<any>();
@ -104,12 +104,12 @@ export class SearchComponent implements OnChanges, OnInit, OnDestroy
takeUntil(this._unsubscribeAll),
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
// so the autocomplete panel can be closed
if ( !value || value.length < this.minLength )
{
this.results = null;
this.resultSets = null;
}
// Continue
@ -121,13 +121,13 @@ export class SearchComponent implements OnChanges, OnInit, OnDestroy
)
.subscribe((value) => {
this._httpClient.post('api/common/search', {query: value})
.subscribe((response: any) => {
.subscribe((resultSets: any) => {
// Store the results
this.results = response.results;
// Store the result sets
this.resultSets = resultSets;
// 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 { defaultNavigation } from 'app/mock-api/common/navigation/data';
import { contacts } from 'app/mock-api/apps/contacts/data';
import { tasks } from 'app/mock-api/apps/tasks/data';
@Injectable({
providedIn: 'root'
@ -12,6 +13,7 @@ export class SearchMockApi
{
private readonly _defaultNavigation: FuseNavigationItem[] = defaultNavigation;
private readonly _contacts: any[] = contacts;
private readonly _tasks: any[] = tasks;
/**
* Constructor
@ -54,58 +56,75 @@ export class SearchMockApi
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
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 = [];
// 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 ( contactsResults.length > 0 )
{
// Normalize the results while marking the found chars
// Normalize the results
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
result.link = '/apps/contacts/' + result.id;
});
// Add the results to the results object
results.push(...contactsResults);
// Add to the results
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 [200, {results}];
return [200, results];
});
}
}