MOBILE-2302 courses: Implement search courses

main
Dani Palou 2017-12-12 15:28:01 +01:00
parent 2d29cc2da6
commit 0c69cbddad
15 changed files with 354 additions and 9 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -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 {}

View File

@ -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>

View File

@ -0,0 +1,6 @@
core-search-box {
.button.item-button[icon-only] {
margin: 0;
padding: ($content-padding / 2) $content-padding;
}
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -0,0 +1,6 @@
core-courses-course-list-item {
.mm-course-enrollment-img {
max-width: 16px;
max-height: 16px;
}
}

View File

@ -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});
}
}

View File

@ -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>

View File

@ -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 {}

View File

@ -0,0 +1,3 @@
page-core-courses-search {
}

View File

@ -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);
});
}
}

View File

@ -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>

View File

@ -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.