MOBILE-2302 courses: Implement search courses
parent
2d29cc2da6
commit
0c69cbddad
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
|
@ -23,6 +23,7 @@ import { CoreShowPasswordComponent } from './show-password/show-password';
|
|||
import { CoreIframeComponent } from './iframe/iframe';
|
||||
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||
import { CoreSearchBoxComponent } from './search-box/search-box';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -32,7 +33,8 @@ import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
|||
CoreShowPasswordComponent,
|
||||
CoreIframeComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreEmptyBoxComponent
|
||||
CoreEmptyBoxComponent,
|
||||
CoreSearchBoxComponent
|
||||
],
|
||||
imports: [
|
||||
IonicModule,
|
||||
|
@ -46,7 +48,8 @@ import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
|||
CoreShowPasswordComponent,
|
||||
CoreIframeComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreEmptyBoxComponent
|
||||
CoreEmptyBoxComponent,
|
||||
CoreSearchBoxComponent
|
||||
]
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<ion-card>
|
||||
<form #f="ngForm" (ngSubmit)="submitForm(f.value.search)">
|
||||
<ion-item>
|
||||
<ion-input type="text" name="search" ngModel [placeholder]="placeholder" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus"></ion-input>
|
||||
<button item-right ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="!f.value.search || (f.value.search.length < lengthCheck)">
|
||||
<ion-icon name="search"></ion-icon>
|
||||
</button>
|
||||
</ion-item>
|
||||
</form>
|
||||
</ion-card>
|
|
@ -0,0 +1,6 @@
|
|||
core-search-box {
|
||||
.button.item-button[icon-only] {
|
||||
margin: 0;
|
||||
padding: ($content-padding / 2) $content-padding;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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: 'search-box.html'
|
||||
})
|
||||
export class CoreSearchBoxComponent implements OnInit {
|
||||
@Input() initialValue?: string = ''; // Initial value for search text.
|
||||
@Input() searchLabel?: string ; // Label to be used on action button.
|
||||
@Input() placeholder?: string; // Placeholder text for search text input.
|
||||
@Input() autocorrect?: string = '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?: number = 3; // Check value length before submit. If 0, any string will be submitted.
|
||||
@Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form.
|
||||
|
||||
constructor(private translate: TranslateService, private utils: CoreUtilsProvider) {
|
||||
this.onSubmit = new EventEmitter();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submitted.
|
||||
*
|
||||
* @param {string} value Entered value.
|
||||
*/
|
||||
submitForm(value: string) {
|
||||
if (value.length < this.lengthCheck) {
|
||||
// The view should handle this case, but we check it here too just in case.
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSubmit.emit(value);
|
||||
}
|
||||
|
||||
}
|
|
@ -19,10 +19,12 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||
import { CoreComponentsModule } from '../../../components/components.module';
|
||||
import { CoreDirectivesModule } from '../../../directives/directives.module';
|
||||
import { CoreCoursesCourseProgressComponent } from '../components/course-progress/course-progress';
|
||||
import { CoreCoursesCourseListItemComponent } from '../components/course-list-item/course-list-item';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCoursesCourseProgressComponent
|
||||
CoreCoursesCourseProgressComponent,
|
||||
CoreCoursesCourseListItemComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -34,7 +36,8 @@ import { CoreCoursesCourseProgressComponent } from '../components/course-progres
|
|||
providers: [
|
||||
],
|
||||
exports: [
|
||||
CoreCoursesCourseProgressComponent
|
||||
CoreCoursesCourseProgressComponent,
|
||||
CoreCoursesCourseListItemComponent
|
||||
]
|
||||
})
|
||||
export class CoreCoursesComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<a ion-item text-wrap (click)="openCourse(course)" [class.item-disabled]="course.visible == 0" [title]="course.fullname">
|
||||
<ion-icon name="ionic" item-start></ion-icon>
|
||||
<h2><core-format-text [text]="course.fullname"></core-format-text></h2>
|
||||
<div item-end>
|
||||
<span *ngIf="!course.isEnrolled">
|
||||
<span ion-button icon-only clear *ngFor="let instance of course.enrollment" [attr.aria-label]=" instance.name | translate">
|
||||
<ion-icon *ngIf="instance.icon" [name]="instance.icon" class="icon-accessory"></ion-icon>
|
||||
<img *ngIf="instance.img && !instance.icon" [src]="instance.img" class="mm-course-enrollment-img">
|
||||
</span>
|
||||
</span>
|
||||
<span ion-button icon-only clear>
|
||||
<ion-icon name="arrow-forward" md="ios-arrow-forward" class="icon-accessory"></ion-icon>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
|
@ -0,0 +1,6 @@
|
|||
core-courses-course-list-item {
|
||||
.mm-course-enrollment-img {
|
||||
max-width: 16px;
|
||||
max-height: 16px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, OnInit } from '@angular/core';
|
||||
import { NavController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreCoursesProvider } from '../../providers/courses';
|
||||
|
||||
/**
|
||||
* This directive is meant to display an item for a list of courses.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-courses-course-list-item [course]="course"></core-courses-course-list-item>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-courses-course-list-item',
|
||||
templateUrl: 'course-list-item.html'
|
||||
})
|
||||
export class CoreCoursesCourseListItemComponent implements OnInit {
|
||||
@Input() course: any; // The course to render.
|
||||
|
||||
constructor(private navCtrl: NavController, private translate: TranslateService, private coursesProvider: CoreCoursesProvider) {}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit() {
|
||||
// Check if the user is enrolled in the course.
|
||||
return this.coursesProvider.getUserCourse(this.course.id).then(() => {
|
||||
this.course.isEnrolled = true;
|
||||
}).catch(() => {
|
||||
this.course.isEnrolled = false;
|
||||
this.course.enrollment = [];
|
||||
|
||||
this.course.enrollmentmethods.forEach((instance) => {
|
||||
if (instance === 'self') {
|
||||
this.course.enrollment.push({
|
||||
name: this.translate.instant('core.courses.selfenrolment'),
|
||||
icon: 'unlock'
|
||||
});
|
||||
} else if (instance === 'guest') {
|
||||
this.course.enrollment.push({
|
||||
name: this.translate.instant('core.courses.allowguests'),
|
||||
icon: 'person'
|
||||
});
|
||||
} else if (instance === 'paypal') {
|
||||
this.course.enrollment.push({
|
||||
name: this.translate.instant('core.courses.paypalaccepted'),
|
||||
img: 'assets/img/icons/paypal.png'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.course.enrollment.length == 0) {
|
||||
this.course.enrollment.push({
|
||||
name: this.translate.instant('core.courses.notenrollable'),
|
||||
icon: 'lock'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a course.
|
||||
*/
|
||||
openCourse(course) {
|
||||
this.navCtrl.push('CoreCoursesCoursePreviewPage', {course: course});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title>{{ 'core.courses.searchcourses' | translate }}</ion-title>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-search-box (onSubmit)="search($event)" [placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" autoFocus="true"></core-search-box>
|
||||
|
||||
<div *ngIf="courses">
|
||||
<ion-item-divider color="light">{{ 'core.courses.totalcoursesearchresults' | translate:{$a: total} }}</ion-item-divider>
|
||||
<core-empty-box *ngIf="total == 0" icon="search" [message]="'core.courses.nosearchresults' | translate"></core-empty-box>
|
||||
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
|
||||
<ion-infinite-scroll [enabled]="canLoadMore" (ionInfinite)="loadMoreResults($event)">
|
||||
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreCoursesSearchPage } from './search';
|
||||
import { CoreComponentsModule } from '../../../../components/components.module';
|
||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCoursesSearchPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreCoursesComponentsModule,
|
||||
IonicPageModule.forChild(CoreCoursesSearchPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class CoreCoursesSearchPageModule {}
|
|
@ -0,0 +1,3 @@
|
|||
page-core-courses-search {
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 } from '@angular/core';
|
||||
import { IonicPage } from 'ionic-angular';
|
||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||
import { CoreCoursesProvider } from '../../providers/courses';
|
||||
|
||||
/**
|
||||
* Page that allows searching for courses.
|
||||
*/
|
||||
@IonicPage()
|
||||
@Component({
|
||||
selector: 'page-core-courses-search',
|
||||
templateUrl: 'search.html',
|
||||
})
|
||||
export class CoreCoursesSearchPage {
|
||||
total = 0;
|
||||
courses: any[];
|
||||
canLoadMore: boolean;
|
||||
|
||||
protected page = 0;
|
||||
protected currentSearch = '';
|
||||
|
||||
constructor(private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider) {}
|
||||
|
||||
/**
|
||||
* Search a new text.
|
||||
*
|
||||
* @param {string} text The text to search.
|
||||
*/
|
||||
search(text: string) {
|
||||
this.currentSearch = text;
|
||||
this.courses = undefined;
|
||||
this.page = 0;
|
||||
|
||||
let modal = this.domUtils.showModalLoading('core.searching', true);
|
||||
this.searchCourses().finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more results.
|
||||
*/
|
||||
loadMoreResults(infiniteScroll) {
|
||||
this.searchCourses().finally(() => {
|
||||
infiniteScroll.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search courses or load the next page of current search.
|
||||
*/
|
||||
protected searchCourses() {
|
||||
return this.coursesProvider.search(this.currentSearch, this.page).then((response) => {
|
||||
if (this.page === 0) {
|
||||
this.courses = response.courses;
|
||||
} else {
|
||||
this.courses = this.courses.concat(response.courses);
|
||||
}
|
||||
this.total = response.total;
|
||||
|
||||
this.page++;
|
||||
this.canLoadMore = this.courses.length < this.total;
|
||||
}).catch((error) => {
|
||||
this.canLoadMore = false;
|
||||
this.domUtils.showErrorModalDefault(error, 'core.courses.errorsearching', true);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
<div *ngIf="!fixedSites">
|
||||
<p padding>{{ 'core.login.newsitedescription' | translate }}</p>
|
||||
<ion-item>
|
||||
<ion-input type="url" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" formControlName="siteUrl" core-auto-focus></ion-input>
|
||||
<ion-input type="url" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" formControlName="siteUrl" [core-auto-focus]></ion-input>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Directive, Input, AfterViewInit, ElementRef } from '@angular/core';
|
||||
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
||||
import { NavController } from 'ionic-angular';
|
||||
import { CoreDomUtilsProvider } from '../providers/utils/dom';
|
||||
import { CoreUtilsProvider } from '../providers/utils/utils';
|
||||
|
||||
|
@ -24,19 +25,35 @@ import { CoreUtilsProvider } from '../providers/utils/utils';
|
|||
@Directive({
|
||||
selector: '[core-auto-focus]'
|
||||
})
|
||||
export class CoreAutoFocusDirective implements AfterViewInit {
|
||||
export class CoreAutoFocusDirective implements OnInit {
|
||||
@Input('core-auto-focus') coreAutoFocus: boolean|string = true;
|
||||
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider) {
|
||||
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
|
||||
private navCtrl: NavController) {
|
||||
this.element = element.nativeElement || element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit() {
|
||||
if (this.navCtrl.isTransitioning()) {
|
||||
// Navigating to a new page. Wait for the transition to be over.
|
||||
let subscription = this.navCtrl.viewDidEnter.subscribe(() => {
|
||||
this.autoFocus();
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
} else {
|
||||
this.autoFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function after the view is initialized.
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
protected autoFocus() {
|
||||
const autoFocus = this.utils.isTrueOrOne(this.coreAutoFocus);
|
||||
if (autoFocus) {
|
||||
// If it's a ion-input or ion-textarea, search the right input to use.
|
||||
|
|
Loading…
Reference in New Issue