diff --git a/src/assets/img/icons/paypal.png b/src/assets/img/icons/paypal.png new file mode 100644 index 000000000..058f6e4cb Binary files /dev/null and b/src/assets/img/icons/paypal.png differ diff --git a/src/components/components.module.ts b/src/components/components.module.ts index ab0b08f84..c814ce0ec 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -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 {} diff --git a/src/components/search-box/search-box.html b/src/components/search-box/search-box.html new file mode 100644 index 000000000..bc0b8a5da --- /dev/null +++ b/src/components/search-box/search-box.html @@ -0,0 +1,10 @@ + +
+ + + + +
+
diff --git a/src/components/search-box/search-box.scss b/src/components/search-box/search-box.scss new file mode 100644 index 000000000..d451c7559 --- /dev/null +++ b/src/components/search-box/search-box.scss @@ -0,0 +1,6 @@ +core-search-box { + .button.item-button[icon-only] { + margin: 0; + padding: ($content-padding / 2) $content-padding; + } +} diff --git a/src/components/search-box/search-box.ts b/src/components/search-box/search-box.ts new file mode 100644 index 000000000..a6e70b3f9 --- /dev/null +++ b/src/components/search-box/search-box.ts @@ -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: + * + */ +@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; // 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); + } + +} diff --git a/src/core/courses/components/components.module.ts b/src/core/courses/components/components.module.ts index f6a66ff29..6cddb3414 100644 --- a/src/core/courses/components/components.module.ts +++ b/src/core/courses/components/components.module.ts @@ -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 {} diff --git a/src/core/courses/components/course-list-item/course-list-item.html b/src/core/courses/components/course-list-item/course-list-item.html new file mode 100644 index 000000000..1c105977b --- /dev/null +++ b/src/core/courses/components/course-list-item/course-list-item.html @@ -0,0 +1,15 @@ + + +

+
+ + + + + + + + + +
+
diff --git a/src/core/courses/components/course-list-item/course-list-item.scss b/src/core/courses/components/course-list-item/course-list-item.scss new file mode 100644 index 000000000..2487cb6e9 --- /dev/null +++ b/src/core/courses/components/course-list-item/course-list-item.scss @@ -0,0 +1,6 @@ +core-courses-course-list-item { + .mm-course-enrollment-img { + max-width: 16px; + max-height: 16px; + } +} diff --git a/src/core/courses/components/course-list-item/course-list-item.ts b/src/core/courses/components/course-list-item/course-list-item.ts new file mode 100644 index 000000000..9e67707f1 --- /dev/null +++ b/src/core/courses/components/course-list-item/course-list-item.ts @@ -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: + * + * + */ +@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}); + } + +} diff --git a/src/core/courses/pages/search/search.html b/src/core/courses/pages/search/search.html new file mode 100644 index 000000000..4df0a3845 --- /dev/null +++ b/src/core/courses/pages/search/search.html @@ -0,0 +1,18 @@ + + + {{ 'core.courses.searchcourses' | translate }} + + + + + +
+ {{ '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 new file mode 100644 index 000000000..c74212c21 --- /dev/null +++ b/src/core/courses/pages/search/search.module.ts @@ -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 {} diff --git a/src/core/courses/pages/search/search.scss b/src/core/courses/pages/search/search.scss new file mode 100644 index 000000000..1bf3fe798 --- /dev/null +++ b/src/core/courses/pages/search/search.scss @@ -0,0 +1,3 @@ +page-core-courses-search { + +} diff --git a/src/core/courses/pages/search/search.ts b/src/core/courses/pages/search/search.ts new file mode 100644 index 000000000..654b36cb5 --- /dev/null +++ b/src/core/courses/pages/search/search.ts @@ -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); + }); + } +} diff --git a/src/core/login/pages/site/site.html b/src/core/login/pages/site/site.html index d8f15beac..dc5c1dda1 100644 --- a/src/core/login/pages/site/site.html +++ b/src/core/login/pages/site/site.html @@ -19,7 +19,7 @@

{{ 'core.login.newsitedescription' | translate }}

- +
diff --git a/src/directives/auto-focus.ts b/src/directives/auto-focus.ts index 5cded27fa..9b41e7a3c 100644 --- a/src/directives/auto-focus.ts +++ b/src/directives/auto-focus.ts @@ -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.