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()">
|
||||
<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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user