mirror of
				https://github.com/richard-loafle/fuse-angular.git
				synced 2025-11-04 08:23:33 +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,22 +22,36 @@
 | 
			
		||||
            (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">
 | 
			
		||||
                <mat-option
 | 
			
		||||
                    class="group relative h-14 px-6 py-0 sm:px-8 text-md"
 | 
			
		||||
                    [routerLink]="result.link">
 | 
			
		||||
                    <ng-container
 | 
			
		||||
                        [ngTemplateOutlet]="searchResult"
 | 
			
		||||
                        [ngTemplateOutletContext]="{$implicit: result}"></ng-container>
 | 
			
		||||
                </mat-option>
 | 
			
		||||
            <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 mb-1 py-0 px-6 text-md rounded-md hover:bg-gray-100 dark:hover:bg-hover"
 | 
			
		||||
                        [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>
 | 
			
		||||
        </mat-autocomplete>
 | 
			
		||||
        <button
 | 
			
		||||
@ -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">
 | 
			
		||||
                <mat-option
 | 
			
		||||
                    class="group relative h-14 px-5 py-0 text-md"
 | 
			
		||||
                    [routerLink]="result.link">
 | 
			
		||||
                    <ng-container
 | 
			
		||||
                        [ngTemplateOutlet]="searchResult"
 | 
			
		||||
                        [ngTemplateOutletContext]="{$implicit: result}"></ng-container>
 | 
			
		||||
                </mat-option>
 | 
			
		||||
            <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 mb-1 py-0 px-6 text-md rounded-md hover:bg-gray-100 dark:hover:bg-hover"
 | 
			
		||||
                        [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>
 | 
			
		||||
        </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">
 | 
			
		||||
                <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 class="flex items-center">
 | 
			
		||||
        <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 dark:text-primary-400"
 | 
			
		||||
                *ngIf="!result.avatar"
 | 
			
		||||
                [svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
 | 
			
		||||
        </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>
 | 
			
		||||
        <div class="ml-3 truncate">
 | 
			
		||||
            <span [innerHTML]="result.name"></span>
 | 
			
		||||
        </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>
 | 
			
		||||
 | 
			
		||||
@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user