-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MOBILE-3371 search: Filter global search
- Loading branch information
1 parent
5e08921
commit b5804f7
Showing
11 changed files
with
472 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
236 changes: 236 additions & 0 deletions
236
src/core/features/search/components/global-search-filters/global-search-filters.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
// (C) Copyright 2015 Moodle Pty Ltd. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
import { Component, OnInit, Input } from '@angular/core'; | ||
import { CoreEnrolledCourseData, CoreCourses } from '@features/courses/services/courses'; | ||
import { | ||
CoreSearchGlobalSearchFilters, | ||
CoreSearchGlobalSearch, | ||
CoreSearchGlobalSearchSearchAreaCategory, | ||
CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED, | ||
} from '@features/search/services/global-search'; | ||
import { CoreEvents } from '@singletons/events'; | ||
import { ModalController } from '@singletons'; | ||
|
||
type Filter<T=unknown> = T & { checked: boolean }; | ||
|
||
@Component({ | ||
selector: 'core-search-global-search-filters', | ||
templateUrl: 'global-search-filters.html', | ||
styleUrls: ['./global-search-filters.scss'], | ||
}) | ||
export class CoreSearchGlobalSearchFiltersComponent implements OnInit { | ||
|
||
allSearchAreaCategories: boolean | null = true; | ||
searchAreaCategories: Filter<CoreSearchGlobalSearchSearchAreaCategory>[] = []; | ||
allCourses: boolean | null = true; | ||
courses: Filter<CoreEnrolledCourseData>[] = []; | ||
|
||
@Input() filters?: CoreSearchGlobalSearchFilters; | ||
|
||
private newFilters: CoreSearchGlobalSearchFilters = {}; | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
async ngOnInit(): Promise<void> { | ||
this.newFilters = this.filters ?? {}; | ||
|
||
// Fetch search area categories. | ||
const searchAreas = await CoreSearchGlobalSearch.getSearchAreas(); | ||
const searchAreaCategoryIds = new Set(); | ||
|
||
this.searchAreaCategories = []; | ||
|
||
for (const searchArea of searchAreas) { | ||
if (searchAreaCategoryIds.has(searchArea.category.id)) { | ||
continue; | ||
} | ||
|
||
searchAreaCategoryIds.add(searchArea.category.id); | ||
this.searchAreaCategories.push({ | ||
...searchArea.category, | ||
checked: this.filters?.searchAreaCategoryIds?.includes(searchArea.category.id) ?? true, | ||
}); | ||
} | ||
|
||
this.allSearchAreaCategories = this.getGroupFilterStatus(this.searchAreaCategories); | ||
|
||
// Fetch courses. | ||
const courses = await CoreCourses.getUserCourses(true); | ||
|
||
this.courses = courses | ||
.sort((a, b) => (a.shortname?.toLowerCase() ?? '').localeCompare(b.shortname?.toLowerCase() ?? '')) | ||
.map(course => ({ | ||
...course, | ||
checked: this.filters?.courseIds?.includes(course.id) ?? true, | ||
})); | ||
|
||
this.allCourses = this.getGroupFilterStatus(this.courses); | ||
} | ||
|
||
/** | ||
* Close popover. | ||
*/ | ||
close(): void { | ||
ModalController.dismiss(); | ||
} | ||
|
||
/** | ||
* Checkbox for all search area categories has been updated. | ||
*/ | ||
allSearchAreaCategoriesUpdated(): void { | ||
if (this.allSearchAreaCategories === null) { | ||
return; | ||
} | ||
|
||
const checked = this.allSearchAreaCategories; | ||
|
||
this.searchAreaCategories.forEach(searchAreaCategory => { | ||
if (searchAreaCategory.checked === checked) { | ||
return; | ||
} | ||
|
||
searchAreaCategory.checked = checked; | ||
}); | ||
} | ||
|
||
/** | ||
* Checkbox for one search area category has been updated. | ||
* | ||
* @param searchAreaCategory Filter status. | ||
*/ | ||
onSearchAreaCategoryInputChanged(searchAreaCategory: Filter<CoreSearchGlobalSearchSearchAreaCategory>): void { | ||
if ( | ||
!searchAreaCategory.checked && | ||
this.newFilters.searchAreaCategoryIds && | ||
!this.newFilters.searchAreaCategoryIds.includes(searchAreaCategory.id) | ||
) { | ||
return; | ||
} | ||
|
||
if ( | ||
searchAreaCategory.checked && | ||
(!this.newFilters.searchAreaCategoryIds || this.newFilters.searchAreaCategoryIds.includes(searchAreaCategory.id)) | ||
) { | ||
return; | ||
} | ||
|
||
this.searchAreaCategoryUpdated(); | ||
} | ||
|
||
/** | ||
* Checkbox for all courses has been updated. | ||
*/ | ||
allCoursesUpdated(): void { | ||
if (this.allCourses === null) { | ||
return; | ||
} | ||
|
||
const checked = this.allCourses; | ||
|
||
this.courses.forEach(course => { | ||
if (course.checked === checked) { | ||
return; | ||
} | ||
|
||
course.checked = checked; | ||
}); | ||
} | ||
|
||
/** | ||
* Checkbox for one course has been updated. | ||
* | ||
* @param course Filter status. | ||
*/ | ||
onCourseInputChanged(course: Filter<CoreEnrolledCourseData>): void { | ||
if (!course.checked && this.newFilters.courseIds && !this.newFilters.courseIds.includes(course.id)) { | ||
return; | ||
} | ||
|
||
if (course.checked && (!this.newFilters.courseIds || this.newFilters.courseIds.includes(course.id))) { | ||
return; | ||
} | ||
|
||
this.courseUpdated(); | ||
} | ||
|
||
/** | ||
* Checkbox for one search area category has been updated. | ||
*/ | ||
private searchAreaCategoryUpdated(): void { | ||
const filterStatus = this.getGroupFilterStatus(this.searchAreaCategories); | ||
|
||
if (filterStatus !== this.allSearchAreaCategories) { | ||
this.allSearchAreaCategories = filterStatus; | ||
} | ||
|
||
this.emitFiltersUpdated(); | ||
} | ||
|
||
/** | ||
* Course filter status has been updated. | ||
*/ | ||
private courseUpdated(): void { | ||
const filterStatus = this.getGroupFilterStatus(this.courses); | ||
|
||
if (filterStatus !== this.allCourses) { | ||
this.allCourses = filterStatus; | ||
} | ||
|
||
this.emitFiltersUpdated(); | ||
} | ||
|
||
/** | ||
* Get the status for a filter representing a group of filters. | ||
* | ||
* @param filters Filters in the group. | ||
* @returns Group filter status | ||
*/ | ||
private getGroupFilterStatus(filters: Filter[]): boolean | null { | ||
if (filters.length === 0) { | ||
return null; | ||
} | ||
|
||
const firstChecked = filters[0].checked; | ||
|
||
for (const filter of filters) { | ||
if (filter.checked === firstChecked) { | ||
continue; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
return firstChecked; | ||
} | ||
|
||
/** | ||
* Emit filters updated event. | ||
*/ | ||
private emitFiltersUpdated(): void { | ||
this.newFilters = {}; | ||
|
||
if (!this.allSearchAreaCategories) { | ||
this.newFilters.searchAreaCategoryIds = this.searchAreaCategories.filter(({ checked }) => checked).map(({ id }) => id); | ||
} | ||
|
||
if (!this.allCourses) { | ||
this.newFilters.courseIds = this.courses.filter(({ checked }) => checked).map(({ id }) => id); | ||
} | ||
|
||
CoreEvents.trigger(CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED, this.newFilters); | ||
} | ||
|
||
} |
55 changes: 55 additions & 0 deletions
55
src/core/features/search/components/global-search-filters/global-search-filters.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<ion-header> | ||
<ion-toolbar> | ||
<ion-title> | ||
<h1>{{ 'core.search.filterheader' | translate }}</h1> | ||
</ion-title> | ||
<ion-buttons slot="end"> | ||
<ion-button fill="clear" (click)="close()" [attr.aria-label]="'core.close' | translate"> | ||
<ion-icon name="fas-xmark" slot="icon-only" aria-hidden=true></ion-icon> | ||
</ion-button> | ||
</ion-buttons> | ||
</ion-toolbar> | ||
</ion-header> | ||
<ion-content [fullscreen]="true"> | ||
<ion-list> | ||
<ng-container *ngIf="searchAreaCategories.length > 0"> | ||
<core-spacer></core-spacer> | ||
<ion-item class="ion-text-wrap help"> | ||
<ion-label> | ||
{{ 'core.search.filtercategories' | translate }} | ||
</ion-label> | ||
</ion-item> | ||
<ion-item class="ion-text-wrap"> | ||
<ion-label>{{ 'core.search.allcategories' | translate }}</ion-label> | ||
<ion-checkbox slot="end" [(ngModel)]="allSearchAreaCategories" [indeterminate]="allSearchAreaCategories === null" | ||
(ionChange)="allSearchAreaCategoriesUpdated()"></ion-checkbox> | ||
</ion-item> | ||
<ion-item class="ion-text-wrap" *ngFor="let searchAreaCategory of searchAreaCategories"> | ||
<ion-label> | ||
<core-format-text [text]="searchAreaCategory.name"></core-format-text> | ||
</ion-label> | ||
<ion-checkbox slot="end" [(ngModel)]="searchAreaCategory.checked" | ||
(ionChange)="onSearchAreaCategoryInputChanged(searchAreaCategory)"></ion-checkbox> | ||
</ion-item> | ||
</ng-container> | ||
<ng-container *ngIf="courses.length > 0"> | ||
<core-spacer></core-spacer> | ||
<ion-item class="ion-text-wrap help"> | ||
<ion-label> | ||
{{ 'core.search.filtercourses' | translate }} | ||
</ion-label> | ||
</ion-item> | ||
<ion-item class="ion-text-wrap"> | ||
<ion-label>{{ 'core.search.allcourses' | translate }}</ion-label> | ||
<ion-checkbox slot="end" [(ngModel)]="allCourses" [indeterminate]="allCourses === null" (ionChange)="allCoursesUpdated()"> | ||
</ion-checkbox> | ||
</ion-item> | ||
<ion-item class="ion-text-wrap" *ngFor="let course of courses"> | ||
<ion-label> | ||
<core-format-text [text]="course.shortname"></core-format-text> | ||
</ion-label> | ||
<ion-checkbox slot="end" [(ngModel)]="course.checked" (ionChange)="onCourseInputChanged(course)"></ion-checkbox> | ||
</ion-item> | ||
</ng-container> | ||
</ion-list> | ||
</ion-content> |
30 changes: 30 additions & 0 deletions
30
src/core/features/search/components/global-search-filters/global-search-filters.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// (C) Copyright 2015 Moodle Pty Ltd. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
import { CoreSharedModule } from '@/core/shared.module'; | ||
import { NgModule } from '@angular/core'; | ||
|
||
import { CoreSearchGlobalSearchFiltersComponent } from './global-search-filters.component'; | ||
|
||
export { CoreSearchGlobalSearchFiltersComponent }; | ||
|
||
@NgModule({ | ||
imports: [ | ||
CoreSharedModule, | ||
], | ||
declarations: [ | ||
CoreSearchGlobalSearchFiltersComponent, | ||
], | ||
}) | ||
export class CoreSearchGlobalSearchFiltersComponentModule {} |
21 changes: 21 additions & 0 deletions
21
src/core/features/search/components/global-search-filters/global-search-filters.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
:host { | ||
--help-text-color: var(--gray-700); | ||
|
||
ion-item.help { | ||
color: var(--help-text-color); | ||
|
||
ion-label { | ||
margin-bottom: 0; | ||
} | ||
|
||
} | ||
|
||
ion-item:not(.help) { | ||
font-size: 16px; | ||
} | ||
|
||
} | ||
|
||
:host-context(html.dark) { | ||
--help-text-color: var(--gray-400); | ||
} |
Oops, something went wrong.