mirror of
https://github.com/richard-loafle/fuse-angular.git
synced 2024-10-30 09:18:46 +00:00
(layouts/common/search) Improved the autocomplete design
This commit is contained in:
parent
39650d3cc9
commit
c1c9904b9d
|
@ -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,23 +22,37 @@
|
||||||
(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-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
|
<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">
|
[routerLink]="result.link">
|
||||||
<ng-container
|
<!-- Contacts -->
|
||||||
[ngTemplateOutlet]="searchResult"
|
<ng-container *ngIf="resultSet.id === 'contacts'">
|
||||||
[ngTemplateOutletContext]="{$implicit: result}"></ng-container>
|
<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>
|
</mat-option>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
</mat-autocomplete>
|
</mat-autocomplete>
|
||||||
<button
|
<button
|
||||||
class="absolute top-1/2 right-5 sm:right-7 flex-shrink-0 w-10 h-10 -mt-5"
|
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"
|
[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-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
|
<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">
|
[routerLink]="result.link">
|
||||||
<ng-container
|
<!-- Contacts -->
|
||||||
[ngTemplateOutlet]="searchResult"
|
<ng-container *ngIf="resultSet.id === 'contacts'">
|
||||||
[ngTemplateOutletContext]="{$implicit: result}"></ng-container>
|
<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>
|
</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>
|
||||||
|
|
||||||
<!-- 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="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="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="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">
|
|
||||||
<img
|
<img
|
||||||
*ngIf="result.avatar"
|
*ngIf="result.avatar"
|
||||||
[src]="result.avatar">
|
[src]="result.avatar">
|
||||||
<mat-icon
|
<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"
|
*ngIf="!result.avatar"
|
||||||
[svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
|
[svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="ml-3 truncate">
|
||||||
</ng-container>
|
<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>
|
</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>
|
</ng-template>
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user