MOBILE-3281 search: Add history to search boxes
parent
67da1cdeba
commit
17448fe010
|
@ -19,6 +19,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { CoreComponentsModule } from '@components/components.module';
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
import { CorePipesModule } from '@pipes/pipes.module';
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@core/search/components/components.module';
|
||||||
import { AddonMessagesDiscussionsComponent } from '../components/discussions/discussions';
|
import { AddonMessagesDiscussionsComponent } from '../components/discussions/discussions';
|
||||||
import { AddonMessagesConfirmedContactsComponent } from '../components/confirmed-contacts/confirmed-contacts';
|
import { AddonMessagesConfirmedContactsComponent } from '../components/confirmed-contacts/confirmed-contacts';
|
||||||
import { AddonMessagesContactRequestsComponent } from '../components/contact-requests/contact-requests';
|
import { AddonMessagesContactRequestsComponent } from '../components/contact-requests/contact-requests';
|
||||||
|
@ -37,7 +38,8 @@ import { AddonMessagesContactsComponent } from '../components/contacts/contacts'
|
||||||
TranslateModule.forChild(),
|
TranslateModule.forChild(),
|
||||||
CoreComponentsModule,
|
CoreComponentsModule,
|
||||||
CoreDirectivesModule,
|
CoreDirectivesModule,
|
||||||
CorePipesModule
|
CorePipesModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
],
|
],
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { AddonMessagesSearchPage } from './search';
|
||||||
import { CoreComponentsModule } from '@components/components.module';
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
import { CorePipesModule } from '@pipes/pipes.module';
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@core/search/components/components.module';
|
||||||
import { AddonMessagesComponentsModule } from '../../components/components.module';
|
import { AddonMessagesComponentsModule } from '../../components/components.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -29,9 +30,10 @@ import { AddonMessagesComponentsModule } from '../../components/components.modul
|
||||||
CoreComponentsModule,
|
CoreComponentsModule,
|
||||||
CoreDirectivesModule,
|
CoreDirectivesModule,
|
||||||
CorePipesModule,
|
CorePipesModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
AddonMessagesComponentsModule,
|
AddonMessagesComponentsModule,
|
||||||
IonicPageModule.forChild(AddonMessagesSearchPage),
|
IonicPageModule.forChild(AddonMessagesSearchPage),
|
||||||
TranslateModule.forChild()
|
TranslateModule.forChild(),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AddonMessagesSearchPageModule {}
|
export class AddonMessagesSearchPageModule {}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module';
|
||||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
import { CorePipesModule } from '@pipes/pipes.module';
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@core/search/components/components.module';
|
||||||
import { AddonModGlossaryIndexComponent } from './index/index';
|
import { AddonModGlossaryIndexComponent } from './index/index';
|
||||||
import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-picker';
|
import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-picker';
|
||||||
|
|
||||||
|
@ -35,7 +36,8 @@ import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-p
|
||||||
CoreComponentsModule,
|
CoreComponentsModule,
|
||||||
CoreDirectivesModule,
|
CoreDirectivesModule,
|
||||||
CorePipesModule,
|
CorePipesModule,
|
||||||
CoreCourseComponentsModule
|
CoreCourseComponentsModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
],
|
],
|
||||||
|
|
|
@ -87,6 +87,7 @@ import { CoreRatingModule } from '@core/rating/rating.module';
|
||||||
import { CoreTagModule } from '@core/tag/tag.module';
|
import { CoreTagModule } from '@core/tag/tag.module';
|
||||||
import { CoreFilterModule } from '@core/filter/filter.module';
|
import { CoreFilterModule } from '@core/filter/filter.module';
|
||||||
import { CoreH5PModule } from '@core/h5p/h5p.module';
|
import { CoreH5PModule } from '@core/h5p/h5p.module';
|
||||||
|
import { CoreSearchModule } from '@core/search/search.module';
|
||||||
|
|
||||||
// Addon modules.
|
// Addon modules.
|
||||||
import { AddonBadgesModule } from '@addon/badges/badges.module';
|
import { AddonBadgesModule } from '@addon/badges/badges.module';
|
||||||
|
@ -235,6 +236,7 @@ export const WP_PROVIDER: any = null;
|
||||||
CoreTagModule,
|
CoreTagModule,
|
||||||
CoreFilterModule,
|
CoreFilterModule,
|
||||||
CoreH5PModule,
|
CoreH5PModule,
|
||||||
|
CoreSearchModule,
|
||||||
AddonBadgesModule,
|
AddonBadgesModule,
|
||||||
AddonBlogModule,
|
AddonBlogModule,
|
||||||
AddonCalendarModule,
|
AddonCalendarModule,
|
||||||
|
|
|
@ -112,13 +112,15 @@ ion-app.app-root {
|
||||||
@include core-selected-item($core-splitview-selected);
|
@include core-selected-item($core-splitview-selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recover borders on items inside cards.
|
// Recover borders on items inside cards and lists.
|
||||||
.card.with-borders .core-as-item,
|
.card.with-borders .core-as-item,
|
||||||
|
.list.with-borders .core-as-item,
|
||||||
.core-as-item {
|
.core-as-item {
|
||||||
@include core-as-items();
|
@include core-as-items();
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.with-borders .item {
|
.card.with-borders .item,
|
||||||
|
.list.with-borders .item {
|
||||||
@include core-items();
|
@include core-items();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { CoreSplitViewComponent } from './split-view/split-view';
|
||||||
import { CoreIframeComponent } from './iframe/iframe';
|
import { CoreIframeComponent } from './iframe/iframe';
|
||||||
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||||
import { CoreSearchBoxComponent } from './search-box/search-box';
|
|
||||||
import { CoreFileComponent } from './file/file';
|
import { CoreFileComponent } from './file/file';
|
||||||
import { CoreFilesComponent } from './files/files';
|
import { CoreFilesComponent } from './files/files';
|
||||||
import { CoreIconComponent } from './icon/icon';
|
import { CoreIconComponent } from './icon/icon';
|
||||||
|
@ -66,7 +65,6 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
|
||||||
CoreIframeComponent,
|
CoreIframeComponent,
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
CoreSearchBoxComponent,
|
|
||||||
CoreFileComponent,
|
CoreFileComponent,
|
||||||
CoreFilesComponent,
|
CoreFilesComponent,
|
||||||
CoreIconComponent,
|
CoreIconComponent,
|
||||||
|
@ -118,7 +116,6 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
|
||||||
CoreIframeComponent,
|
CoreIframeComponent,
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
CoreSearchBoxComponent,
|
|
||||||
CoreFileComponent,
|
CoreFileComponent,
|
||||||
CoreFilesComponent,
|
CoreFilesComponent,
|
||||||
CoreIconComponent,
|
CoreIconComponent,
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
// (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, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component to display a "search box".
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This component will display a standalone search box with its search button in order to have a better UX.
|
|
||||||
*
|
|
||||||
* Example usage:
|
|
||||||
* <core-search-box (onSubmit)="search($event)" [placeholder]="'core.courses.search' | translate"
|
|
||||||
* [searchLabel]="'core.courses.search' | translate" autoFocus="true"></core-search-box>
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'core-search-box',
|
|
||||||
templateUrl: 'core-search-box.html'
|
|
||||||
})
|
|
||||||
export class CoreSearchBoxComponent implements OnInit {
|
|
||||||
@Input() searchLabel?: string; // Label to be used on action button.
|
|
||||||
@Input() placeholder?: string; // Placeholder text for search text input.
|
|
||||||
@Input() autocorrect = 'on'; // Enables/disable Autocorrection on search text input.
|
|
||||||
@Input() spellcheck?: string | boolean = true; // Enables/disable Spellchecker on search text input.
|
|
||||||
@Input() autoFocus?: string | boolean; // Enables/disable Autofocus when entering view.
|
|
||||||
@Input() lengthCheck = 3; // Check value length before submit. If 0, any string will be submitted.
|
|
||||||
@Input() showClear = true; // Show/hide clear button.
|
|
||||||
@Input() disabled = false; // Disables the input text.
|
|
||||||
@Input() initialSearch: string; // Initial search text.
|
|
||||||
@Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form.
|
|
||||||
@Output() onClear: EventEmitter<void>; // Send event when clearing the search form.
|
|
||||||
|
|
||||||
searched = false;
|
|
||||||
searchText = '';
|
|
||||||
|
|
||||||
constructor(private translate: TranslateService, private utils: CoreUtilsProvider) {
|
|
||||||
this.onSubmit = new EventEmitter<string>();
|
|
||||||
this.onClear = new EventEmitter<void>();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.searchLabel = this.searchLabel || this.translate.instant('core.search');
|
|
||||||
this.placeholder = this.placeholder || this.translate.instant('core.search');
|
|
||||||
this.spellcheck = this.utils.isTrueOrOne(this.spellcheck);
|
|
||||||
this.showClear = this.utils.isTrueOrOne(this.showClear);
|
|
||||||
this.searchText = this.initialSearch || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Form submitted.
|
|
||||||
*
|
|
||||||
* @param e Event.
|
|
||||||
*/
|
|
||||||
submitForm(e: Event): void {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (this.searchText.length < this.lengthCheck) {
|
|
||||||
// The view should handle this case, but we check it here too just in case.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searched = true;
|
|
||||||
this.onSubmit.emit(this.searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Form submitted.
|
|
||||||
*/
|
|
||||||
clearForm(): void {
|
|
||||||
this.searched = false;
|
|
||||||
this.searchText = '';
|
|
||||||
this.onClear.emit();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { CoreCoursesSearchPage } from './search';
|
import { CoreCoursesSearchPage } from './search';
|
||||||
import { CoreComponentsModule } from '@components/components.module';
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@core/search/components/components.module';
|
||||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -28,8 +29,9 @@ import { CoreCoursesComponentsModule } from '../../components/components.module'
|
||||||
CoreComponentsModule,
|
CoreComponentsModule,
|
||||||
CoreDirectivesModule,
|
CoreDirectivesModule,
|
||||||
CoreCoursesComponentsModule,
|
CoreCoursesComponentsModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
IonicPageModule.forChild(CoreCoursesSearchPage),
|
IonicPageModule.forChild(CoreCoursesSearchPage),
|
||||||
TranslateModule.forChild()
|
TranslateModule.forChild(),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreCoursesSearchPageModule {}
|
export class CoreCoursesSearchPageModule {}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
// (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 { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreSearchBoxComponent } from './search-box/search-box';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreSearchBoxComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreComponentsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CoreSearchBoxComponent,
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
CoreSearchBoxComponent,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CoreSearchComponentsModule {}
|
|
@ -1,13 +1,21 @@
|
||||||
<ion-card>
|
<ion-card>
|
||||||
<form #f="ngForm" (ngSubmit)="submitForm($event)" role="search">
|
<form #f="ngForm" (ngSubmit)="submitForm($event)" role="search">
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-input type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [disabled]="disabled" role="searchbox"></ion-input>
|
<ion-input type="search" name="search" [(ngModel)]="searchText" [placeholder]="placeholder" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus" [disabled]="disabled" role="searchbox" (focus)="showHistory()" (blur)="hideHistory()"></ion-input>
|
||||||
<button item-end ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="disabled || !searchText || (searchText.length < lengthCheck)">
|
<button item-end ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="disabled || !searchText || (searchText.length < lengthCheck)">
|
||||||
<ion-icon name="search"></ion-icon>
|
<ion-icon name="search"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="showClear" item-end ion-button clear icon-only class="button-small" [attr.aria-label]="'core.clearsearch' | translate" [disabled]="!searched || disabled" (click)="clearForm()">
|
<button *ngIf="showClear" item-end ion-button clear icon-only class="button-small" [attr.aria-label]="'core.clearsearch' | translate" [disabled]="searched == '' || disabled" (click)="clearForm()">
|
||||||
<ion-icon name="close"></ion-icon>
|
<ion-icon name="close"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<div class="core-search-history" *ngIf="historyShown && history.length">
|
||||||
|
<ion-list class="with-borders">
|
||||||
|
<ion-item text-wrap *ngFor="let item of history" (mousedown)="itemMouseDown($event, item.searchedtext)" (mouseup)="itemMouseUp($event, item.searchedtext)" class="core-clickable">
|
||||||
|
<core-icon name="fa-history" item-start></core-icon>
|
||||||
|
{{item.searchedtext}}
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ion-card>
|
</ion-card>
|
|
@ -1,4 +1,12 @@
|
||||||
ion-app.app-root core-search-box {
|
ion-app.app-root core-search-box {
|
||||||
|
height: 65px;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
ion-card {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
.button.item-button[icon-only] {
|
.button.item-button[icon-only] {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: ($content-padding / 2) $content-padding;
|
padding: ($content-padding / 2) $content-padding;
|
||||||
|
@ -18,4 +26,17 @@ ion-app.app-root core-search-box {
|
||||||
border: 0;
|
border: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.core-search-history {
|
||||||
|
max-height: calc(-120px + 80vh);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.item:hover {
|
||||||
|
background-color: $gray-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .item.item-block:last-child > .item-inner {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,211 @@
|
||||||
|
// (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, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreSearchHistoryProvider, CoreSearchHistoryItem } from '../../providers/search-history';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display a "search box".
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* This component will display a standalone search box with its search button in order to have a better UX.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* <core-search-box (onSubmit)="search($event)" [placeholder]="'core.courses.search' | translate"
|
||||||
|
* [searchLabel]="'core.courses.search' | translate" autoFocus="true"></core-search-box>
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-search-box',
|
||||||
|
templateUrl: 'core-search-box.html'
|
||||||
|
})
|
||||||
|
export class CoreSearchBoxComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() searchLabel?: string; // Label to be used on action button.
|
||||||
|
@Input() placeholder?: string; // Placeholder text for search text input.
|
||||||
|
@Input() autocorrect = 'on'; // Enables/disable Autocorrection on search text input.
|
||||||
|
@Input() spellcheck?: string | boolean = true; // Enables/disable Spellchecker on search text input.
|
||||||
|
@Input() autoFocus?: string | boolean; // Enables/disable Autofocus when entering view.
|
||||||
|
@Input() lengthCheck = 3; // Check value length before submit. If 0, any string will be submitted.
|
||||||
|
@Input() showClear = true; // Show/hide clear button.
|
||||||
|
@Input() disabled = false; // Disables the input text.
|
||||||
|
@Input() protected initialSearch: string; // Initial search text.
|
||||||
|
@Input() protected searchArea?: string; // If provided. It will save and display a history of searches for this particular Id.
|
||||||
|
// To use different history lists, place different Id.
|
||||||
|
// I.e. AddonMessagesContacts or CoreUserParticipants-6 (using the course Id).
|
||||||
|
@Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form.
|
||||||
|
@Output() onClear: EventEmitter<void>; // Send event when clearing the search form.
|
||||||
|
|
||||||
|
searched = ''; // Last search emitted.
|
||||||
|
searchText = '';
|
||||||
|
history: CoreSearchHistoryItem[];
|
||||||
|
historyShown = false;
|
||||||
|
|
||||||
|
protected elementClicked = '';
|
||||||
|
protected backdropMouseUpFunc;
|
||||||
|
|
||||||
|
constructor(protected translate: TranslateService,
|
||||||
|
protected utils: CoreUtilsProvider,
|
||||||
|
protected searchHistoryProvider: CoreSearchHistoryProvider,
|
||||||
|
) {
|
||||||
|
this.onSubmit = new EventEmitter<string>();
|
||||||
|
this.onClear = new EventEmitter<void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.searchLabel = this.searchLabel || this.translate.instant('core.search');
|
||||||
|
this.placeholder = this.placeholder || this.translate.instant('core.search');
|
||||||
|
this.spellcheck = this.utils.isTrueOrOne(this.spellcheck);
|
||||||
|
this.showClear = this.utils.isTrueOrOne(this.showClear);
|
||||||
|
this.searchText = this.initialSearch || '';
|
||||||
|
|
||||||
|
if (this.searchArea) {
|
||||||
|
this.loadHistory();
|
||||||
|
|
||||||
|
this.backdropMouseUpFunc = this.backdropMouseUp.bind(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form submitted.
|
||||||
|
*
|
||||||
|
* @param e Event.
|
||||||
|
*/
|
||||||
|
submitForm(e?: Event): void {
|
||||||
|
e && e.preventDefault();
|
||||||
|
e && e.stopPropagation();
|
||||||
|
|
||||||
|
if (this.searchText.length < this.lengthCheck) {
|
||||||
|
// The view should handle this case, but we check it here too just in case.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.searchArea) {
|
||||||
|
this.saveSearchToHistory(this.searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searched = this.searchText;
|
||||||
|
this.onSubmit.emit(this.searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the search term onto the history.
|
||||||
|
*
|
||||||
|
* @param text Text to save.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async saveSearchToHistory(text: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.searchHistoryProvider.insertOrUpdateSearchText(this.searchArea, text);
|
||||||
|
} finally {
|
||||||
|
this.loadHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads search history.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadHistory(): Promise<void> {
|
||||||
|
this.history = await this.searchHistoryProvider.getSearchHistory(this.searchArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show search history.
|
||||||
|
*/
|
||||||
|
showHistory(): void {
|
||||||
|
if (this.searchArea) {
|
||||||
|
this.historyShown = true;
|
||||||
|
this.elementClicked = '';
|
||||||
|
document.body.addEventListener('mouseup', this.backdropMouseUpFunc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide search history.
|
||||||
|
*
|
||||||
|
* @param force: If force hidding the history without checking the clicked element.
|
||||||
|
*/
|
||||||
|
hideHistory(force: boolean = false): void {
|
||||||
|
if (this.searchArea && (force || this.elementClicked == '')) {
|
||||||
|
this.historyShown = false;
|
||||||
|
this.elementClicked = '';
|
||||||
|
|
||||||
|
document.body.removeEventListener('mouseup', this.backdropMouseUpFunc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the item where you moused down.
|
||||||
|
* It uses mouseup/down because blur will block the click event.
|
||||||
|
*
|
||||||
|
* @param e Event.
|
||||||
|
* @param text Selected text.
|
||||||
|
*/
|
||||||
|
itemMouseDown(e: Event, text: string): void {
|
||||||
|
this.elementClicked = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select an item and use it for search text.
|
||||||
|
* It uses mouseup/down because blur will block the click event.
|
||||||
|
*
|
||||||
|
* @param e Event.
|
||||||
|
* @param text Selected text.
|
||||||
|
*/
|
||||||
|
itemMouseUp(e: Event, text: string): void {
|
||||||
|
if (this.elementClicked == text) {
|
||||||
|
this.hideHistory(true);
|
||||||
|
if (this.searched != text) {
|
||||||
|
this.searchText = text;
|
||||||
|
this.submitForm(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.elementClicked = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages mouseup out of the history.
|
||||||
|
*
|
||||||
|
* @param e Event.
|
||||||
|
*/
|
||||||
|
backdropMouseUp(e: Event): void {
|
||||||
|
// Do not hide if the search box is focused.
|
||||||
|
if (document.activeElement['type'] && document.activeElement['type'] == 'search') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hideHistory();
|
||||||
|
this.elementClicked = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form submitted.
|
||||||
|
*/
|
||||||
|
clearForm(): void {
|
||||||
|
this.searched = '';
|
||||||
|
this.searchText = '';
|
||||||
|
this.hideHistory(true);
|
||||||
|
this.onClear.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On destroy of the component, clear up any listeners.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.backdropMouseUpFunc) {
|
||||||
|
document.body.removeEventListener('mouseup', this.backdropMouseUpFunc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
|
||||||
|
import { SQLiteDB } from '@classes/sqlitedb';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search history item definition.
|
||||||
|
*/
|
||||||
|
export interface CoreSearchHistoryItem {
|
||||||
|
searcharea: string; // Search area where the search has been performed.
|
||||||
|
lastused?: number; // Timestamp of the last search.
|
||||||
|
searchedtext: string; // Text of the performed search.
|
||||||
|
times?: number; // Times search has been performed (if previously in history).
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that enables adding a history to a search box.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CoreSearchHistoryProvider {
|
||||||
|
|
||||||
|
protected static HISTORY_TABLE = 'seach_history';
|
||||||
|
protected static HISTORY_LIMIT = 10;
|
||||||
|
|
||||||
|
protected siteSchema: CoreSiteSchema = {
|
||||||
|
name: 'CoreSearchHistoryProvider',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: CoreSearchHistoryProvider.HISTORY_TABLE,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'searcharea',
|
||||||
|
type: 'TEXT',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastused',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'times',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'searchedtext',
|
||||||
|
type: 'TEXT',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primaryKeys: ['searcharea', 'searchedtext'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(protected sitesProvider: CoreSitesProvider) {
|
||||||
|
|
||||||
|
this.sitesProvider.registerSiteSchema(this.siteSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a search area history sorted by use.
|
||||||
|
*
|
||||||
|
* @param searchArea Search Area Name.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the list of items when done.
|
||||||
|
*/
|
||||||
|
async getSearchHistory(searchArea: string, siteId?: string): Promise<CoreSearchHistoryItem[]> {
|
||||||
|
const site = await this.sitesProvider.getSite(siteId);
|
||||||
|
const conditions: any = {
|
||||||
|
searcharea: searchArea,
|
||||||
|
};
|
||||||
|
|
||||||
|
const history = await site.getDb().getRecords(CoreSearchHistoryProvider.HISTORY_TABLE, conditions);
|
||||||
|
|
||||||
|
// Sorting by last used DESC.
|
||||||
|
return history.sort((a, b) => {
|
||||||
|
return b.lastused - a.lastused;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls search limit and removes the last item if overflows.
|
||||||
|
*
|
||||||
|
* @param searchArea Search area to control
|
||||||
|
* @param db SQLite DB where to perform the search.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async controlSearchLimit(searchArea: string, db: SQLiteDB): Promise<void> {
|
||||||
|
const items = await this.getSearchHistory(searchArea);
|
||||||
|
if (items.length > CoreSearchHistoryProvider.HISTORY_LIMIT) {
|
||||||
|
// Over the limit. Remove the last.
|
||||||
|
const lastItem = items.pop();
|
||||||
|
|
||||||
|
const searchItem: CoreSearchHistoryItem = {
|
||||||
|
searcharea: lastItem.searcharea,
|
||||||
|
searchedtext: lastItem.searchedtext,
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.deleteRecords(CoreSearchHistoryProvider.HISTORY_TABLE, searchItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the search history item if exists.
|
||||||
|
*
|
||||||
|
* @param searchArea Area where the search has been performed.
|
||||||
|
* @param text Text of the performed text.
|
||||||
|
* @param db SQLite DB where to perform the search.
|
||||||
|
* @return True if exists, false otherwise.
|
||||||
|
*/
|
||||||
|
protected async updateExistingItem(searchArea: string, text: string, db: SQLiteDB): Promise<boolean> {
|
||||||
|
const searchItem: CoreSearchHistoryItem = {
|
||||||
|
searcharea: searchArea,
|
||||||
|
searchedtext: text,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingItem = await db.getRecord(CoreSearchHistoryProvider.HISTORY_TABLE, searchItem);
|
||||||
|
|
||||||
|
// If item exist, update time and number of times searched.
|
||||||
|
existingItem.lastused = Date.now();
|
||||||
|
existingItem.times++;
|
||||||
|
|
||||||
|
await db.updateRecords(CoreSearchHistoryProvider.HISTORY_TABLE, existingItem, searchItem);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a searched term on the history.
|
||||||
|
*
|
||||||
|
* @param searchArea Area where the search has been performed.
|
||||||
|
* @param text Text of the performed text.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
async insertOrUpdateSearchText(searchArea: string, text: string, siteId?: string): Promise<void> {
|
||||||
|
const site = await this.sitesProvider.getSite(siteId);
|
||||||
|
const db = site.getDb();
|
||||||
|
|
||||||
|
const exists = await this.updateExistingItem(searchArea, text, db);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
// If item is new, control the history does not goes over the limit.
|
||||||
|
const searchItem: CoreSearchHistoryItem = {
|
||||||
|
searcharea: searchArea,
|
||||||
|
searchedtext: text,
|
||||||
|
lastused: Date.now(),
|
||||||
|
times: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
await site.getDb().insertRecord(CoreSearchHistoryProvider.HISTORY_TABLE, searchItem);
|
||||||
|
|
||||||
|
await this.controlSearchLimit(searchArea, db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { NgModule } from '@angular/core';
|
||||||
|
import { CoreSearchComponentsModule } from './components/components.module';
|
||||||
|
import { CoreSearchHistoryProvider } from './providers/search-history';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreSearchComponentsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
CoreSearchComponentsModule,
|
||||||
|
CoreSearchHistoryProvider,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreSearchModule {}
|
|
@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { CoreTagSearchPage } from './search';
|
import { CoreTagSearchPage } from './search';
|
||||||
import { CoreComponentsModule } from '@components/components.module';
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@core/search/components/components.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -26,6 +27,7 @@ import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
imports: [
|
imports: [
|
||||||
CoreComponentsModule,
|
CoreComponentsModule,
|
||||||
CoreDirectivesModule,
|
CoreDirectivesModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
IonicPageModule.forChild(CoreTagSearchPage),
|
IonicPageModule.forChild(CoreTagSearchPage),
|
||||||
TranslateModule.forChild()
|
TranslateModule.forChild()
|
||||||
],
|
],
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { CoreUserTagAreaComponent } from './tag-area/tag-area';
|
||||||
import { CoreComponentsModule } from '@components/components.module';
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
import { CorePipesModule } from '@pipes/pipes.module';
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@core/search/components/components.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -35,7 +36,8 @@ import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
TranslateModule.forChild(),
|
TranslateModule.forChild(),
|
||||||
CoreComponentsModule,
|
CoreComponentsModule,
|
||||||
CoreDirectivesModule,
|
CoreDirectivesModule,
|
||||||
CorePipesModule
|
CorePipesModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in New Issue