{{ 'core.courses.totalcoursesearchresults' | translate:{$a: total} }}
diff --git a/src/core/courses/pages/search/search.module.ts b/src/core/courses/pages/search/search.module.ts
index 57a51cb46..56d61d396 100644
--- a/src/core/courses/pages/search/search.module.ts
+++ b/src/core/courses/pages/search/search.module.ts
@@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreCoursesSearchPage } from './search';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
+import { CoreSearchComponentsModule } from '@core/search/components/components.module';
import { CoreCoursesComponentsModule } from '../../components/components.module';
@NgModule({
@@ -28,8 +29,9 @@ import { CoreCoursesComponentsModule } from '../../components/components.module'
CoreComponentsModule,
CoreDirectivesModule,
CoreCoursesComponentsModule,
+ CoreSearchComponentsModule,
IonicPageModule.forChild(CoreCoursesSearchPage),
- TranslateModule.forChild()
+ TranslateModule.forChild(),
],
})
export class CoreCoursesSearchPageModule {}
diff --git a/src/core/search/components/components.module.ts b/src/core/search/components/components.module.ts
new file mode 100644
index 000000000..70776e0ff
--- /dev/null
+++ b/src/core/search/components/components.module.ts
@@ -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 {}
diff --git a/src/components/search-box/core-search-box.html b/src/core/search/components/search-box/core-search-box.html
similarity index 54%
rename from src/components/search-box/core-search-box.html
rename to src/core/search/components/search-box/core-search-box.html
index 84a8e4b74..adc00c50b 100644
--- a/src/components/search-box/core-search-box.html
+++ b/src/core/search/components/search-box/core-search-box.html
@@ -1,13 +1,21 @@
diff --git a/src/components/search-box/search-box.scss b/src/core/search/components/search-box/search-box.scss
similarity index 56%
rename from src/components/search-box/search-box.scss
rename to src/core/search/components/search-box/search-box.scss
index 1e80c19f2..58eab7287 100644
--- a/src/components/search-box/search-box.scss
+++ b/src/core/search/components/search-box/search-box.scss
@@ -1,4 +1,12 @@
ion-app.app-root core-search-box {
+ height: 65px;
+ display: block;
+
+ ion-card {
+ position: absolute;
+ z-index: 4;
+ }
+
.button.item-button[icon-only] {
margin: 0;
padding: ($content-padding / 2) $content-padding;
@@ -18,4 +26,17 @@ ion-app.app-root core-search-box {
border: 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;
+ }
+ }
}
diff --git a/src/core/search/components/search-box/search-box.ts b/src/core/search/components/search-box/search-box.ts
new file mode 100644
index 000000000..da5223e37
--- /dev/null
+++ b/src/core/search/components/search-box/search-box.ts
@@ -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:
+ *
+ */
+@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
; // Send data when submitting the search form.
+ @Output() onClear: EventEmitter; // 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();
+ this.onClear = new EventEmitter();
+ }
+
+ 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 {
+ try {
+ await this.searchHistoryProvider.insertOrUpdateSearchText(this.searchArea, text);
+ } finally {
+ this.loadHistory();
+ }
+ }
+
+ /**
+ * Loads search history.
+ *
+ * @return Promise resolved when done.
+ */
+ protected async loadHistory(): Promise {
+ 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);
+ }
+ }
+}
diff --git a/src/core/search/providers/search-history.ts b/src/core/search/providers/search-history.ts
new file mode 100644
index 000000000..b70e91cb7
--- /dev/null
+++ b/src/core/search/providers/search-history.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ 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);
+ }
+ }
+}
diff --git a/src/core/search/search.module.ts b/src/core/search/search.module.ts
new file mode 100644
index 000000000..4026fb898
--- /dev/null
+++ b/src/core/search/search.module.ts
@@ -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 {}
diff --git a/src/core/tag/pages/search/search.html b/src/core/tag/pages/search/search.html
index 78747f2a2..0ae40733d 100644
--- a/src/core/tag/pages/search/search.html
+++ b/src/core/tag/pages/search/search.html
@@ -10,7 +10,7 @@
1 ? '' : null">
-
+
1">
diff --git a/src/core/tag/pages/search/search.module.ts b/src/core/tag/pages/search/search.module.ts
index 084c9605c..f199aa6c6 100644
--- a/src/core/tag/pages/search/search.module.ts
+++ b/src/core/tag/pages/search/search.module.ts
@@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreTagSearchPage } from './search';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
+import { CoreSearchComponentsModule } from '@core/search/components/components.module';
@NgModule({
declarations: [
@@ -26,6 +27,7 @@ import { CoreDirectivesModule } from '@directives/directives.module';
imports: [
CoreComponentsModule,
CoreDirectivesModule,
+ CoreSearchComponentsModule,
IonicPageModule.forChild(CoreTagSearchPage),
TranslateModule.forChild()
],
diff --git a/src/core/user/components/components.module.ts b/src/core/user/components/components.module.ts
index a2f772ed8..8c3029a51 100644
--- a/src/core/user/components/components.module.ts
+++ b/src/core/user/components/components.module.ts
@@ -22,6 +22,7 @@ import { CoreUserTagAreaComponent } from './tag-area/tag-area';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
+import { CoreSearchComponentsModule } from '@core/search/components/components.module';
@NgModule({
declarations: [
@@ -35,7 +36,8 @@ import { CorePipesModule } from '@pipes/pipes.module';
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
- CorePipesModule
+ CorePipesModule,
+ CoreSearchComponentsModule,
],
providers: [
],
diff --git a/src/core/user/components/participants/core-user-participants.html b/src/core/user/components/participants/core-user-participants.html
index 78b894138..5b84592eb 100644
--- a/src/core/user/components/participants/core-user-participants.html
+++ b/src/core/user/components/participants/core-user-participants.html
@@ -10,7 +10,7 @@
-
+