Merge pull request #1228 from dpalou/MOBILE-2309

Mobile 2309
main
Juan Leyva 2018-01-24 15:32:11 +01:00 committed by GitHub
commit 4c292da21d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 2769 additions and 276 deletions

View File

@ -7,7 +7,7 @@
<ion-refresher [enabled]="eventLoaded" (ionRefresh)="refreshEvent($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="eventLoaded" class="core-loading-center">
<core-loading [hideUntil]="eventLoaded">
<ion-card>
<ion-card-content>
<ion-card-title text-wrap>
@ -40,7 +40,7 @@
<ion-card list *ngIf="notificationsEnabled">
<ion-item>
<ion-label>{{ 'addon.calendar.notifications' | translate }}</ion-label>
<ion-select [(ngModel)]="notificationTime" (ionChange)="updateNotificationTime($event)">
<ion-select [(ngModel)]="notificationTime" (ionChange)="updateNotificationTime($event)" interface="popover">
<ion-option value="-1">{{ 'core.defaultvalue' | translate :{$a: defaultTimeReadable} }}</ion-option>
<ion-option value="0">{{ 'core.settings.disabled' | translate }}</ion-option>
<ion-option value="10">{{ 600 | coreDuration }}</ion-option>

View File

@ -16,7 +16,7 @@
<ion-refresher [enabled]="eventsLoaded" (ionRefresh)="refreshEvents($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="eventsLoaded" class="core-loading-center">
<core-loading [hideUntil]="eventsLoaded">
<core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate">
</core-empty-box>

View File

@ -7,7 +7,7 @@
<ion-list>
<ion-item>
<ion-label>{{ 'addon.calendar.defaultnotificationtime' | translate }}</ion-label>
<ion-select [(ngModel)]="defaultTime" (ionChange)="updateDefaultTime($event)">
<ion-select [(ngModel)]="defaultTime" (ionChange)="updateDefaultTime($event)" interface="popover">
<ion-option value="0">{{ 'core.settings.disabled' | translate }}</ion-option>
<ion-option value="10">{{ 600 | coreDuration }}</ion-option>
<ion-option value="30">{{ 1800 | coreDuration }}</ion-option>

View File

@ -16,6 +16,11 @@
color: $toolbar-ios-button-color;
}
.item-ios ion-spinner[item-start],
.item-ios ion-spinner[item-end] {
@include margin($item-ios-padding-icon-top, null, $item-ios-padding-icon-bottom, 0);
}
// Highlights inside the input element.
@if ($core-text-input-ios-show-highlight) {
.card-ios, .list-ios {

View File

@ -16,6 +16,11 @@
color: $toolbar-md-button-color;
}
.item-md ion-spinner[item-start] + .item-inner,
.item-md ion-spinner[item-start] + .item-input {
@include margin-horizontal($item-md-padding-start + ($item-md-padding-start / 2) - 1, null);
}
// Highlights inside the input element.
@if ($core-text-input-md-show-highlight) {
.card-md, .list-md {

View File

@ -57,6 +57,8 @@ import { CoreCoursesModule } from '../core/courses/courses.module';
import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module';
import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module';
import { CoreCourseModule } from '../core/course/course.module';
import { CoreSiteHomeModule } from '../core/sitehome/sitehome.module';
import { CoreContentLinksModule } from '../core/contentlinks/contentlinks.module';
// Addon modules.
import { AddonCalendarModule } from '../addon/calendar/calendar.module';
@ -92,6 +94,8 @@ export function createTranslateLoader(http: HttpClient) {
CoreFileUploaderModule,
CoreSharedFilesModule,
CoreCourseModule,
CoreSiteHomeModule,
CoreContentLinksModule,
AddonCalendarModule
],
bootstrap: [IonicApp],

View File

@ -15,3 +15,8 @@
.bar-buttons core-context-menu .button-clear-wp {
color: $toolbar-wp-button-color;
}
.item-wp ion-spinner[item-start] + .item-inner,
.item-wp ion-spinner[item-start] + .item-input {
@include margin-horizontal(($item-wp-padding-start / 2), null);
}

View File

@ -15,8 +15,8 @@ core-loading {
}
}
.scroll-content > .padding > core-loading > .core-loading-container,
ion-content[padding] > .scroll-content > core-loading > .core-loading-container,
.scroll-content > core-loading > .core-loading-container,
ion-content > .scroll-content > core-loading > .core-loading-container,
.core-loading-center .core-loading-container {
position: absolute;
top: 0;

View File

@ -26,6 +26,14 @@ import { TranslateService } from '@ngx-translate/core';
* <core-loading [message]="loadingMessage" [hideUntil]="dataLoaded">
* <!-- CONTENT TO HIDE UNTIL LOADED -->
* </core-loading>
*
* IMPORTANT: Due to how ng-content works in Angular, the content of core-loading will be executed as soon as your view
* is loaded, even if the content hidden. So if you have the following code:
* <core-loading [hideUntil]="dataLoaded"><my-component></my-component></core-loading>
*
* The component "my-component" will be initialized immediately, even if dataLoaded is false, but it will be hidden. If you want
* your component to be initialized only if dataLoaded is true, then you should use ngIf:
* <core-loading [hideUntil]="dataLoaded"><my-component *ngIf="dataLoaded"></my-component></core-loading>
*/
@Component({
selector: 'core-loading',

View File

@ -1,6 +1,6 @@
<ion-item>
<ion-label>{{ 'core.site' | translate }}</ion-label>
<ion-select [(ngModel)]="selectedSite" (ngModelChange)="siteSelected.emit(selectedSite)">
<ion-select [(ngModel)]="selectedSite" (ngModelChange)="siteSelected.emit(selectedSite)" interface="popover">
<ion-option *ngFor="let site of sites" [value]="site.id">{{ site.fullNameAndSiteName }}</ion-option>
</ion-select>
</ion-item>

View File

@ -12,25 +12,33 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter } from '@angular/core';
import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter, ContentChild, TemplateRef,
ViewChild } from '@angular/core';
import { CoreTabsComponent } from './tabs';
import { Content } from 'ionic-angular';
/**
* A tab to use inside core-tabs. The content of this tab will be displayed when the tab is selected.
*
* You must provide either a title or an icon for the tab.
*
* The tab content MUST be surrounded by ng-template. This component uses ngTemplateOutlet instead of ng-content because the
* latter executes all the code immediately. This means that all the tabs would be initialized as soon as your view is
* loaded, leading to performance issues.
*
* Example usage:
*
* <core-tabs selectedIndex="1">
* <core-tab [title]="'core.courses.timeline' | translate" (ionSelect)="switchTab('timeline')">
* <ng-template> <!-- This ng-template is required. -->
* <!-- Tab contents. -->
* </ng-template>
* </core-tab>
* </core-tabs>
*/
@Component({
selector: 'core-tab',
template: '<ng-content></ng-content>'
template: '<ng-container *ngIf="loaded" [ngTemplateOutlet]="template"></ng-container>'
})
export class CoreTabComponent implements OnInit, OnDestroy {
@Input() title?: string; // The tab title.
@ -42,7 +50,11 @@ export class CoreTabComponent implements OnInit, OnDestroy {
@Input() id?: string; // An ID to identify the tab.
@Output() ionSelect: EventEmitter<CoreTabComponent> = new EventEmitter<CoreTabComponent>();
@ContentChild(TemplateRef) template: TemplateRef<any> // Template defined by the content.
@ContentChild(Content) scroll: Content;
element: HTMLElement; // The core-tab element.
loaded = false;
constructor(private tabs: CoreTabsComponent, element: ElementRef) {
this.element = element.nativeElement;
@ -61,4 +73,30 @@ export class CoreTabComponent implements OnInit, OnDestroy {
ngOnDestroy() {
this.tabs.removeTab(this);
}
/**
* Select tab.
*/
selectTab() {
this.element.classList.add('selected');
this.loaded = true;
this.ionSelect.emit(this);
// Setup tab scrolling.
setTimeout(() => {
if (this.scroll) {
this.scroll.getScrollElement().onscroll = (e) => {
this.tabs.showHideTabs(e);
}
}
}, 1);
}
/**
* Unselect tab.
*/
unselectTab() {
this.element.classList.remove('selected');
}
}

View File

@ -1,4 +1,5 @@
<div class="core-tabs-bar">
<core-loading [hideUntil]="hideUntil" class="core-loading-center">
<div class="core-tabs-bar" #topTabs>
<ng-container *ngFor="let tab of tabs; let idx = index">
<a *ngIf="tab.show" [attr.aria-selected]="selected == idx" (click)="selectTab(idx)">
<ion-icon *ngIf="tab.icon" [name]="tab.icon"></ion-icon>
@ -7,6 +8,7 @@
</a>
</ng-container>
</div>
<div #originalTabs>
<div class="core-tabs-content-container" #originalTabs>
<ng-content></ng-content>
</div>
</core-loading>

View File

@ -1,7 +1,6 @@
core-tabs {
.core-tabs-bar {
@include position(null, null, 0, 0);
z-index: $z-index-toolbar;
display: flex;
width: 100%;
@ -21,12 +20,29 @@ core-tabs {
}
}
}
.core-tabs-content-container {
height: 100%;
}
&.tabs-hidden {
.core-tabs-bar {
display: none !important;
}
.core-tabs-content-container {
padding-bottom: 0 !important;
}
}
core-tab {
display: none;
height: 100%;
&.selected {
display: block;
}
}
}
.scroll-content.no-scroll {
overflow: hidden !important;
}

View File

@ -12,8 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, ViewChild, ElementRef,
SimpleChange } from '@angular/core';
import { CoreTabComponent } from './tab';
import { Content } from 'ionic-angular';
/**
* This component displays some tabs that usually share data between them.
@ -25,7 +27,9 @@ import { CoreTabComponent } from './tab';
*
* <core-tabs selectedIndex="1">
* <core-tab [title]="'core.courses.timeline' | translate" (ionSelect)="switchTab('timeline')">
* <ng-template> <!-- This ng-template is required, @see CoreTabComponent. -->
* <!-- Tab contents. -->
* </ng-template>
* </core-tab>
* </core-tabs>
*
@ -35,45 +39,65 @@ import { CoreTabComponent } from './tab';
selector: 'core-tabs',
templateUrl: 'tabs.html'
})
export class CoreTabsComponent implements OnInit, AfterViewInit {
export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
@Input() selectedIndex?: number = 0; // Index of the tab to select.
@Input() hideUntil: boolean; // Determine when should the contents be shown.
@Output() ionChange: EventEmitter<CoreTabComponent> = new EventEmitter<CoreTabComponent>(); // Emitted when the tab changes.
@ViewChild('originalTabs') originalTabsRef: ElementRef;
@ViewChild('topTabs') topTabs: ElementRef;
tabs: CoreTabComponent[] = []; // List of tabs.
selected: number; // Selected tab number.
protected originalTabsContainer: HTMLElement; // The container of the original tabs. It will include each tab's content.
protected initialized = false;
protected afterViewInitTriggered = false;
constructor() {}
protected topTabsElement: HTMLElement; // The container of the original tabs. It will include each tab's content.
protected tabBarHeight;
protected tabBarElement: HTMLElement; // Host element.
protected tabsShown = true;
protected scroll: HTMLElement; // Parent scroll element (if core-tabs is inside a ion-content).
constructor(element: ElementRef, content: Content) {
this.tabBarElement = element.nativeElement;
setTimeout(() => {
if (content) {
this.scroll = content.getScrollElement();
}
}, 1);
}
/**
* Component being initialized.
*/
ngOnInit() {
this.originalTabsContainer = this.originalTabsRef.nativeElement;
this.topTabsElement = this.topTabs.nativeElement;
}
/**
* View has been initialized.
*/
ngAfterViewInit() {
let selectedIndex = this.selectedIndex || 0,
selectedTab = this.tabs[selectedIndex];
if (!selectedTab.enabled || !selectedTab.show) {
// The tab is not enabled or not shown. Get the first tab that is enabled.
selectedTab = this.tabs.find((tab, index) => {
if (tab.enabled && tab.show) {
selectedIndex = index;
return true;
this.afterViewInitTriggered = true;
if (!this.initialized && this.hideUntil) {
// Tabs should be shown, initialize them.
this.initializeTabs();
}
return false;
}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}) {
// We need to wait for ngAfterViewInit because we need core-tab components to be executed.
if (!this.initialized && this.hideUntil && this.afterViewInitTriggered) {
// Tabs should be shown, initialize them.
// Use a setTimeout so child core-tab update their inputs before initializing the tabs.
setTimeout(() => {
this.initializeTabs();
});
}
if (selectedTab) {
this.selectTab(selectedIndex);
}
}
/**
@ -114,6 +138,57 @@ export class CoreTabsComponent implements OnInit, AfterViewInit {
return this.tabs[this.selected];
}
/**
* Initialize the tabs, determining the first tab to be shown.
*/
protected initializeTabs() : void {
let selectedIndex = this.selectedIndex || 0,
selectedTab = this.tabs[selectedIndex];
if (!selectedTab.enabled || !selectedTab.show) {
// The tab is not enabled or not shown. Get the first tab that is enabled.
selectedTab = this.tabs.find((tab, index) => {
if (tab.enabled && tab.show) {
selectedIndex = index;
return true;
}
return false;
});
}
if (selectedTab) {
this.selectTab(selectedIndex);
}
// Setup tab scrolling.
this.tabBarHeight = this.topTabsElement.offsetHeight;
this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px';
if (this.scroll) {
this.scroll.classList.add('no-scroll');
}
this.initialized = true;
}
/**
* Show or hide the tabs. This is used when the user is scrolling inside a tab.
*
* @param {any} e Scroll event.
*/
showHideTabs(e: any) : void {
if (e.target.scrollTop < this.tabBarHeight) {
if (!this.tabsShown) {
this.tabBarElement.classList.remove('tabs-hidden');
this.tabsShown = true;
}
} else {
if (this.tabsShown) {
this.tabBarElement.classList.add('tabs-hidden');
this.tabsShown = false;
}
}
}
/**
* Remove a tab from the list of tabs.
*
@ -140,7 +215,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit {
index = 0;
}
const currenTab = this.getSelected(),
const currentTab = this.getSelected(),
newTab = this.tabs[index];
if (!newTab.enabled || !newTab.show) {
@ -148,14 +223,13 @@ export class CoreTabsComponent implements OnInit, AfterViewInit {
return;
}
if (currenTab) {
if (currentTab) {
// Unselect previous selected tab.
currenTab.element.classList.remove('selected');
currentTab.unselectTab();
}
this.selected = index;
newTab.element.classList.add('selected');
newTab.ionSelect.emit(newTab);
newTab.selectTab();
this.ionChange.emit(newTab);
}

View File

@ -0,0 +1,111 @@
// (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 { CoreContentLinksHandler, CoreContentLinksAction } from '../providers/delegate';
/**
* Base handler to be registered in CoreContentLinksHandler. It is useful to minimize the amount of
* functions that handlers need to implement.
*
* It allows you to specify a "pattern" (RegExp) that will be used to check if the handler handles a URL and to get its site URL.
*/
export class CoreContentLinksHandlerBase implements CoreContentLinksHandler {
/**
* A name to identify the handler.
* @type {string}
*/
name = 'CoreContentLinksHandlerBase';
/**
* Handler's priority. The highest priority is treated first.
* @type {number}
*/
priority = 0;
/**
* Whether the isEnabled function should be called for all the users in a site. It should be true only if the isEnabled call
* can return different values for different users in same site.
* @type {boolean}
*/
checkAllUsers = false;
/**
* Name of the feature this handler is related to.
* It will be used to check if the feature is disabled (@see CoreSite.isFeatureDisabled).
* @type {string}
*/
featureName = '';
/**
* A pattern to use to detect if the handler handles a URL and to get its site URL. Required if "handles" and
* "getSiteUrl" functions aren't overridden.
* @type {RexExp}
*/
pattern?: RegExp;
constructor() {}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
return [];
}
/**
* Check if a URL is handled by this handler.
*
* @param {string} url The URL to check.
* @return {boolean} Whether the URL is handled by this handler
*/
handles(url: string) : boolean {
return this.pattern && url.search(this.pattern) >= 0;
}
/**
* If the URL is handled by this handler, return the site URL.
*
* @param {string} url The URL to check.
* @return {string} Site URL if it is handled, undefined otherwise.
*/
getSiteUrl(url: string) : string {
if (this.pattern) {
var position = url.search(this.pattern);
if (position > -1) {
return url.substr(0, position);
}
}
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise<boolean> {
return true;
}
}

View File

@ -0,0 +1,103 @@
// (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 { NavController } from 'ionic-angular';
import { CoreContentLinksAction } from '../providers/delegate';
import { CoreContentLinksHandlerBase } from './base-handler';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreCourseHelperProvider } from '../../course/providers/helper';
/**
* Handler to handle URLs pointing to the grade of a module.
*/
export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerBase {
/**
* Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled.
* @type {string}
*/
addon: string;
/**
* Name of the module (assign, book, ...).
* @type {string}
*/
modName: string;
/**
* Whether the module can be reviewed in the app. If true, the handler needs to implement the goToReview function.
* @type {boolean}
*/
canReview: boolean;
constructor(protected courseHelper: CoreCourseHelperProvider, protected domUtils: CoreDomUtilsProvider,
protected sitesProvider: CoreSitesProvider) {
super();
// Match the grade.php URL with an id param.
this.pattern = new RegExp('\/mod\/' + this.modName + '\/grade\.php.*([\&\?]id=\\d+)');
this.featureName = '$mmCourseDelegate_' + this.addon;
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
courseId = courseId || params.courseid || params.cid;
return [{
action: (siteId, navCtrl?) : void => {
// Check if userid is the site's current user.
const modal = this.domUtils.showModalLoading();
this.sitesProvider.getSite(siteId).then((site) => {
if (!params.userid || params.userid == site.getUserId()) {
// No user specified or current user. Navigate to module.
this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId);
} else if (this.canReview) {
// Use the goToReview function.
this.goToReview(url, params, courseId, siteId, navCtrl);
} else {
// Not current user and cannot review it in the app, open it in browser.
site.openInBrowserWithAutoLogin(url);
}
}).finally(() => {
modal.dismiss();
});
}
}];
}
/**
* Go to the page to review.
*
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} courseId Course ID related to the URL.
* @param {string} siteId Site to use.
* @param {NavController} [navCtrl] Nav Controller to use to navigate.
* @return {Promise<any>} Promise resolved when done.
*/
protected goToReview(url: string, params: any, courseId: number, siteId: string, navCtrl?: NavController) : Promise<any> {
// This function should be overridden.
return Promise.resolve();
}
}

View File

@ -0,0 +1,63 @@
// (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 { CoreContentLinksAction } from '../providers/delegate';
import { CoreContentLinksHandlerBase } from './base-handler';
import { CoreCourseHelperProvider } from '../../course/providers/helper';
/**
* Handler to handle URLs pointing to the index of a module.
*/
export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerBase {
/**
* Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled.
* @type {string}
*/
addon: string;
/**
* Name of the module (assign, book, ...).
* @type {string}
*/
modName: string;
constructor(private courseHelper: CoreCourseHelperProvider) {
super();
// Match the view.php URL with an id param.
this.pattern = new RegExp('\/mod\/' + this.modName + '\/view\.php.*([\&\?]id=\\d+)');
this.featureName = '$mmCourseDelegate_' + this.addon;
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
courseId = courseId || params.courseid || params.cid;
return [{
action: (siteId, navCtrl?) => {
this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId);
}
}];
}
}

View File

@ -0,0 +1,29 @@
// (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 { CoreContentLinksDelegate } from './providers/delegate';
import { CoreContentLinksHelperProvider } from './providers/helper';
@NgModule({
declarations: [],
imports: [
],
providers: [
CoreContentLinksDelegate,
CoreContentLinksHelperProvider
],
exports: []
})
export class CoreContentLinksModule {}

View File

@ -0,0 +1,7 @@
{
"chooseaccount": "Choose account",
"chooseaccounttoopenlink": "Choose an account to open the link with.",
"confirmurlothersite": "This link belongs to another site. Do you want to open it?",
"errornoactions": "Couldn't find an action to perform with this link.",
"errornosites": "Couldn't find any site to handle this link."
}

View File

@ -0,0 +1,24 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'core.contentlinks.chooseaccount' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list>
<ion-item text-wrap>
<p class="item-heading">{{ 'core.contentlinks.chooseaccounttoopenlink' | translate }}</p>
<p>{{ url }}</p>
</ion-item>
<a ion-item *ngFor="let site of sites" (click)="siteClicked(site.id)">
<img [src]="site.avatar" item-start>
<h2>{{site.fullName}}</h2>
<p><core-format-text clean="true" [text]="site.siteName"></core-format-text></p>
<p>{{site.siteUrl}}</p>
</a>
<ion-item>
<button ion-button block (click)="cancel()">{{ 'core.login.cancel' | translate }}</button>
</ion-item>
</ion-list>
</core-loading>
</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 { CoreContentLinksChooseSitePage } from './choose-site';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../../../components/components.module';
import { CoreDirectivesModule } from '../../../../directives/directives.module';
@NgModule({
declarations: [
CoreContentLinksChooseSitePage
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreContentLinksChooseSitePage),
TranslateModule.forChild()
]
})
export class CoreContentLinksChooseSitePageModule {}

View File

@ -0,0 +1,95 @@
// (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, OnInit } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreContentLinksDelegate, CoreContentLinksAction } from '../../providers/delegate';
import { CoreContentLinksHelperProvider } from '../../providers/helper';
/**
* Page to display the list of sites to choose one to perform a content link action.
*/
@IonicPage({segment: 'core-content-links-choose-site'})
@Component({
selector: 'page-core-content-links-choose-site',
templateUrl: 'choose-site.html',
})
export class CoreContentLinksChooseSitePage implements OnInit {
url: string;
sites: any[];
loaded: boolean;
protected action: CoreContentLinksAction;
constructor(private navCtrl: NavController, navParams: NavParams, private contentLinksDelegate: CoreContentLinksDelegate,
private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider,
private contentLinksHelper: CoreContentLinksHelperProvider) {
this.url = navParams.get('url');
}
/**
* Component being initialized.
*/
ngOnInit() {
if (!this.url) {
return this.leaveView();
}
// Get the action to perform.
this.contentLinksDelegate.getActionsFor(this.url).then((actions) => {
this.action = this.contentLinksHelper.getFirstValidAction(actions);
if (!this.action) {
return Promise.reject(null);
}
// Get the sites that can perform the action.
return this.sitesProvider.getSites(this.action.sites).then((sites) => {
this.sites = sites;
});
}).catch(() => {
this.domUtils.showErrorModal('core.contentlinks.errornosites', true);
this.leaveView();
}).finally(() => {
this.loaded = true;
});
}
/**
* Cancel.
*/
cancel() : void {
this.leaveView();
}
/**
* Perform the action on a certain site.
*
* @param {string} siteId Site ID.
*/
siteClicked(siteId: string) : void {
this.action.action(siteId, this.navCtrl);
}
/**
* Cancel and leave the view.
*/
protected leaveView() {
this.sitesProvider.logout().finally(() => {
this.navCtrl.setRoot('CoreLoginSitesPage');
});
}
}

View File

@ -0,0 +1,311 @@
// (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 { Injectable } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreUrlUtilsProvider } from '../../../providers/utils/url';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
/**
* Interface that all handlers must implement.
*/
export interface CoreContentLinksHandler {
/**
* A name to identify the handler.
* @type {string}
*/
name: string;
/**
* Handler's priority. The highest priority is treated first.
* @type {number}
*/
priority?: number;
/**
* Whether the isEnabled function should be called for all the users in a site. It should be true only if the isEnabled call
* can return different values for different users in same site.
* @type {boolean}
*/
checkAllUsers?: boolean;
/**
* Name of the feature this handler is related to.
* It will be used to check if the feature is disabled (@see CoreSite.isFeatureDisabled).
* @type {string}
*/
featureName?: string;
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>;
/**
* Check if a URL is handled by this handler.
*
* @param {string} url The URL to check.
* @return {boolean} Whether the URL is handled by this handler
*/
handles(url: string) : boolean;
/**
* If the URL is handled by this handler, return the site URL.
*
* @param {string} url The URL to check.
* @return {string} Site URL if it is handled, undefined otherwise.
*/
getSiteUrl(url: string) : string;
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled?(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise<boolean>;
};
/**
* Action to perform when a link is clicked.
*/
export interface CoreContentLinksAction {
/**
* A message to identify the action. Default: 'core.view'.
* @type {string}
*/
message?: string;
/**
* Name of the icon of the action. Default: 'eye'.
* @type {string}
*/
icon?: string;
/**
* IDs of the sites that support the action.
* @type {string[]}
*/
sites?: string[];
/**
* Action to perform when the link is clicked.
*
* @param {string} siteId The site ID.
* @param {NavController} [navCtrl] Nav Controller to use to navigate.
*/
action(siteId: string, navCtrl?: NavController) : void;
};
/**
* Actions and priority for a handler and URL.
*/
export interface CoreContentLinksHandlerActions {
/**
* Handler's priority.
* @type {number}
*/
priority: number;
/**
* List of actions.
* @type {CoreContentLinksAction[]}
*/
actions: CoreContentLinksAction[];
};
/**
* Delegate to register handlers to handle links.
*/
@Injectable()
export class CoreContentLinksDelegate {
protected logger;
protected handlers: {[s: string]: CoreContentLinksHandler} = {}; // All registered handlers.
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider,
private utils: CoreUtilsProvider) {
this.logger = logger.getInstance('CoreContentLinksDelegate');
}
/**
* Get the list of possible actions to do for a URL.
*
* @param {string} url URL to handle.
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @param {string} [username] Username to use to filter sites.
* @return {Promise<CoreContentLinksAction[]>} Promise resolved with the actions.
*/
getActionsFor(url: string, courseId?: number, username?: string) : Promise<CoreContentLinksAction[]> {
if (!url) {
return Promise.resolve([]);
}
// Get the list of sites the URL belongs to.
return this.sitesProvider.getSiteIdsFromUrl(url, true, username).then((siteIds) => {
const linkActions: CoreContentLinksHandlerActions[] = [],
promises = [],
params = this.urlUtils.extractUrlParams(url);
for (let name in this.handlers) {
const handler = this.handlers[name],
checkAll = handler.checkAllUsers,
isEnabledFn = this.isHandlerEnabled.bind(this, handler, url, params, courseId);
if (!handler.handles(url)) {
// Invalid handler or it doesn't handle the URL. Stop.
continue;
}
// Filter the site IDs using the isEnabled function.
promises.push(this.utils.filterEnabledSites(siteIds, isEnabledFn, checkAll).then((siteIds) => {
if (!siteIds.length) {
// No sites supported, no actions.
return;
}
return Promise.resolve(handler.getActions(siteIds, url, params, courseId)).then((actions) => {
if (actions && actions.length) {
// Set default values if any value isn't supplied.
actions.forEach((action) => {
action.message = action.message || 'core.view';
action.icon = action.icon || 'eye';
action.sites = action.sites || siteIds;
});
// Add them to the list.
linkActions.push({
priority: handler.priority,
actions: actions
});
}
});
}));
}
return this.utils.allPromises(promises).catch(() => {
// Ignore errors.
}).then(() => {
// Sort link actions by priority.
return this.sortActionsByPriority(linkActions);
});
});
}
/**
* Get the site URL if the URL is supported by any handler.
*
* @param {string} url URL to handle.
* @return {string} Site URL if the URL is supported by any handler, undefined otherwise.
*/
getSiteUrl(url: string) : string {
if (!url) {
return;
}
// Check if any handler supports this URL.
for (let name in this.handlers) {
const handler = this.handlers[name],
siteUrl = handler.getSiteUrl(url);
if (siteUrl) {
return siteUrl;
}
}
}
/**
* Check if a handler is enabled for a certain site and URL.
*
* @param {CoreContentLinksHandler} handler Handler to check.
* @param {string} url The URL to check.
* @param {any} params The params of the URL
* @param {number} courseId Course ID the URL belongs to (can be undefined).
* @param {string} siteId The site ID to check.
* @return {Promise<boolean>} Promise resolved with boolean: whether the handler is enabled.
*/
protected isHandlerEnabled(handler: CoreContentLinksHandler, url: string, params: any, courseId: number, siteId: string)
: Promise<boolean> {
let promise;
if (handler.featureName) {
// Check if the feature is disabled.
promise = this.sitesProvider.isFeatureDisabled(handler.featureName, siteId);
} else {
promise = Promise.resolve(false);
}
return promise.then((disabled) => {
if (disabled) {
return false;
}
if (!handler.isEnabled) {
// isEnabled function not provided, assume it's enabled.
return true;
}
return handler.isEnabled(siteId, url, params, courseId);
});
}
/**
* Register a handler.
*
* @param {CoreContentLinksHandler} handler The handler to register.
* @return {boolean} True if registered successfully, false otherwise.
*/
registerHandler(handler: CoreContentLinksHandler) : boolean {
if (typeof this.handlers[handler.name] !== 'undefined') {
this.logger.log(`Addon '${handler.name}' already registered`);
return false;
}
this.logger.log(`Registered addon '${handler.name}'`);
this.handlers[handler.name] = handler;
return true;
}
/**
* Sort actions by priority.
*
* @param {CoreContentLinksHandlerActions[]} actions Actions to sort.
* @return {CoreContentLinksAction[]} Sorted actions.
*/
protected sortActionsByPriority(actions: CoreContentLinksHandlerActions[]) : CoreContentLinksAction[] {
let sorted: CoreContentLinksAction[] = [];
// Sort by priority.
actions = actions.sort((a, b) => {
return a.priority >= b.priority ? 1 : -1;
});
// Fill result array.
actions.forEach((entry) => {
sorted = sorted.concat(entry.actions);
});
return sorted;
}
}

View File

@ -0,0 +1,240 @@
// (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 { Injectable } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '../../../providers/app';
import { CoreEventsProvider } from '../../../providers/events';
import { CoreInitDelegate } from '../../../providers/init';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreUrlUtilsProvider } from '../../../providers/utils/url';
import { CoreLoginHelperProvider } from '../../login/providers/helper';
import { CoreContentLinksDelegate, CoreContentLinksAction } from './delegate';
import { CoreConstants } from '../../constants';
import { CoreConfigConstants } from '../../../configconstants';
/**
* Service that provides some features regarding content links.
*/
@Injectable()
export class CoreContentLinksHelperProvider {
protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider,
private contentLinksDelegate: CoreContentLinksDelegate, private appProvider: CoreAppProvider,
private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private translate: TranslateService,
private initDelegate: CoreInitDelegate, eventsProvider: CoreEventsProvider) {
this.logger = logger.getInstance('CoreContentLinksHelperProvider');
// Listen for app launched URLs. If we receive one, check if it's a content link.
eventsProvider.on(CoreEventsProvider.APP_LAUNCHED_URL, this.handleCustomUrl.bind(this));
}
/**
* Get the first valid action in a list of actions.
*
* @param {CoreContentLinksAction[]} actions List of actions.
* @return {CoreContentLinksAction} First valid action. Returns undefined if no valid action found.
*/
getFirstValidAction(actions: CoreContentLinksAction[]) : CoreContentLinksAction {
if (actions) {
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
if (action && action.sites && action.sites.length) {
return action;
}
}
}
}
/**
* Goes to a certain page in a certain site. If the site is current site it will perform a regular navigation,
* otherwise it will 'redirect' to the other site.
*
* @param {NavController} navCtrl The NavController instance to use.
* @param {string} pageName Name of the page to go.
* @param {any} [pageParams] Params to send to the page.
* @param {string} [siteId] Site ID. If not defined, current site.
*/
goInSite(navCtrl: NavController, pageName: string, pageParams: any, siteId?: string) : void {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (siteId == this.sitesProvider.getCurrentSiteId()) {
navCtrl.push(pageName, pageParams);
} else {
this.loginHelper.redirect(pageName, pageParams, siteId);
}
}
/**
* Go to the page to choose a site.
*
* @param {string} url URL to treat.
*/
goToChooseSite(url: string) : void {
this.appProvider.getRootNavController().setRoot('CoreContentLinksChooseSitePage', {url: url});
}
/**
* Handle a URL received by Custom URL Scheme.
*
* @param {string} url URL to handle.
* @return {boolean} True if the URL should be handled by this component, false otherwise.
*/
handleCustomUrl(url: string) : boolean {
const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link';
if (url.indexOf(contentLinksScheme) == -1) {
return false;
}
url = decodeURIComponent(url);
// App opened using custom URL scheme.
this.logger.debug('Treating custom URL scheme: ' + url);
let modal = this.domUtils.showModalLoading(),
username;
// Delete the scheme from the URL.
url = url.replace(contentLinksScheme + '=', '');
// Detect if there's a user specified.
username = this.urlUtils.getUsernameFromUrl(url);
if (username) {
url = url.replace(username + '@', ''); // Remove the username from the URL.
}
// Wait for the app to be ready.
this.initDelegate.ready().then(() => {
// Check if the site is stored.
return this.sitesProvider.getSiteIdsFromUrl(url, false, username);
}).then((siteIds) => {
if (siteIds.length) {
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
return this.handleLink(url, username).then((treated) => {
if (!treated) {
this.domUtils.showErrorModal('core.contentlinks.errornoactions', true);
}
});
} else {
// Get the site URL.
const siteUrl = this.contentLinksDelegate.getSiteUrl(url);
if (!siteUrl) {
this.domUtils.showErrorModal('core.login.invalidsite', true);
return;
}
// Check that site exists.
return this.sitesProvider.checkSite(siteUrl).then((result) => {
// Site exists. We'll allow to add it.
let promise,
ssoNeeded = this.loginHelper.isSSOLoginNeeded(result.code),
hasRemoteAddonsLoaded = false,
pageName = 'CoreLoginCredentialsPage',
pageParams = {
siteUrl: result.siteUrl,
username: username,
urlToOpen: url,
siteConfig: result.config
};
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
if (!this.sitesProvider.isLoggedIn()) {
// Not logged in, no need to confirm. If SSO the confirm will be shown later.
promise = Promise.resolve();
} else {
// Ask the user before changing site.
const confirmMsg = this.translate.instant('core.contentlinks.confirmurlothersite');
promise = this.domUtils.showConfirm(confirmMsg).then(() => {
if (!ssoNeeded) {
// hasRemoteAddonsLoaded = $mmAddonManager.hasRemoteAddonsLoaded(); @todo
if (hasRemoteAddonsLoaded) {
// Store the redirect since logout will restart the app.
this.appProvider.storeRedirect(CoreConstants.NO_SITE_ID, pageName, pageParams);
}
return this.sitesProvider.logout().catch(() => {
// Ignore errors (shouldn't happen).
});
}
});
}
return promise.then(() => {
if (ssoNeeded) {
this.loginHelper.confirmAndOpenBrowserForSSOLogin(
result.siteUrl, result.code, result.service, result.config && result.config.launchurl);
} else if (!hasRemoteAddonsLoaded) {
this.appProvider.getRootNavController().setRoot(pageName, pageParams);
}
});
}).catch((error) => {
if (error) {
this.domUtils.showErrorModal(error);
}
});
}
}).finally(() => {
modal.dismiss();
});
return true;
}
/**
* Handle a link.
*
* @param {string} url URL to handle.
* @param {string} [username] Username related with the URL. E.g. in 'http://myuser@m.com', url would be 'http://m.com' and
* the username 'myuser'. Don't use it if you don't want to filter by username.
* @param {NavController} [navCtrl] Nav Controller to use to navigate.
* @return {Promise<boolean>} Promise resolved with a boolean: true if URL was treated, false otherwise.
*/
handleLink(url: string, username?: string, navCtrl?: NavController) : Promise<boolean> {
// Check if the link should be treated by some component/addon.
return this.contentLinksDelegate.getActionsFor(url, undefined, username).then((actions) => {
const action = this.getFirstValidAction(actions);
if (action) {
if (!this.sitesProvider.isLoggedIn()) {
// No current site. Perform the action if only 1 site found, choose the site otherwise.
if (action.sites.length == 1) {
action.action(action.sites[0], navCtrl);
} else {
this.goToChooseSite(url);
}
} else if (action.sites.length == 1 && action.sites[0] == this.sitesProvider.getCurrentSiteId()) {
// Current site.
action.action(action.sites[0], navCtrl);
} else {
// Not current site or more than one site. Ask for confirmation.
this.domUtils.showConfirm(this.translate.instant('core.contentlinks.confirmurlothersite')).then(() => {
if (action.sites.length == 1) {
action.action(action.sites[0], navCtrl);
} else {
this.goToChooseSite(url);
}
});
}
return true;
}
return false;
}).catch(() => {
return false;
});
}
}

View File

@ -12,7 +12,7 @@
<!-- Section selector. -->
<div *ngIf="!componentInstances.sectionSelector && displaySectionSelector && sections && sections.length" no-padding class="clearfix">
<!-- @todo: How to display availabilityinfo and not visible messages? -->
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start>
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start interface="popover">
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
</ion-select>
<!-- Section download. -->

View File

@ -42,6 +42,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
@Input() course: any; // The course to render.
@Input() sections: any[]; // List of course sections.
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
@Input() initialSectionId?: number; // The section to load first (by ID).
@Input() initialSectionNumber?: number; // The section to load first (by number).
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when any module completion changes.
// Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
@ -142,11 +144,24 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: {[name: string]: SimpleChange}) {
if (changes.sections && this.sections) {
if (!this.selectedSection) {
// There is no selected section yet, calculate which one to get.
// There is no selected section yet, calculate which one to load.
if (this.initialSectionId || this.initialSectionNumber) {
// We have an input indicating the section ID to load. Search the section.
for (let i = 0; i < this.sections.length; i++) {
let section = this.sections[i];
if (section.id == this.initialSectionId || section.section == this.initialSectionNumber) {
this.loaded = true;
this.sectionChanged(section);
break;
}
}
} else {
// No section specified, get current section.
this.cfDelegate.getCurrentSection(this.course, this.sections).then((section) => {
this.loaded = true;
this.sectionChanged(section);
});
}
} else {
// We have a selected section, but the list has changed. Search the section in the list.
let newSection;
@ -214,6 +229,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// Set the Input data.
this.componentInstances[type].course = this.course;
this.componentInstances[type].sections = this.sections;
this.componentInstances[type].initialSectionId = this.initialSectionId;
this.componentInstances[type].initialSectionNumber = this.initialSectionNumber;
this.componentInstances[type].downloadEnabled = this.downloadEnabled;
this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed.

View File

@ -21,6 +21,6 @@
<a aria-selected="true">{{ 'core.course.contents' | translate }}</a>
<a *ngFor="let handler of courseHandlers">{{ handler.data.title || translate }}</a>
</div>
<core-course-format [course]="course" [sections]="sections" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format>
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format>
</core-loading>
</ion-content>

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Component, ViewChild, OnDestroy } from '@angular/core';
import { IonicPage, NavParams, Content } from 'ionic-angular';
import { IonicPage, NavParams, Content, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '../../../../providers/events';
import { CoreSitesProvider } from '../../../../providers/sites';
@ -39,6 +39,8 @@ export class CoreCourseSectionPage implements OnDestroy {
title: string;
course: any;
sections: any[];
sectionId: number;
sectionNumber: number;
courseHandlers: CoreCoursesHandlerToDisplay[];
dataLoaded: boolean;
downloadEnabled: boolean;
@ -47,18 +49,18 @@ export class CoreCourseSectionPage implements OnDestroy {
prefetchCourseIcon: 'spinner'
};
protected moduleId;
protected completionObserver;
protected courseStatusObserver;
protected isDestroyed = false;
constructor(navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
constructor(private navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
private courseFormatDelegate: CoreCourseFormatDelegate, private coursesDelegate: CoreCoursesDelegate,
private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider,
private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider,
sitesProvider: CoreSitesProvider) {
sitesProvider: CoreSitesProvider, private navCtrl: NavController) {
this.course = navParams.get('course');
this.moduleId = navParams.get('moduleId');
this.sectionId = navParams.get('sectionId');
this.sectionNumber = navParams.get('sectionNumber');
// Get the title to display. We dont't have sections yet.
this.title = courseFormatDelegate.getCourseTitle(this.course);
@ -81,9 +83,14 @@ export class CoreCourseSectionPage implements OnDestroy {
* View loaded.
*/
ionViewDidLoad() {
let module = this.navParams.get('module');
if (module) {
this.courseHelper.openModule(this.navCtrl, module, this.course.id, this.sectionId);
}
this.loadData().finally(() => {
this.dataLoaded = true;
delete this.moduleId; // Only load module automatically the first time.
// Determine the course prefetch status.
this.determineCoursePrefetchIcon().then(() => {
@ -133,7 +140,7 @@ export class CoreCourseSectionPage implements OnDestroy {
promises.push(promise.then((completionStatus) => {
// Get all the sections.
promises.push(this.courseProvider.getSections(this.course.id, false, true).then((sections) => {
this.courseHelper.addHandlerDataForModules(sections, this.course.id, this.moduleId, completionStatus);
this.courseHelper.addHandlerDataForModules(sections, this.course.id, completionStatus);
// Format the name of each section and check if it has content.
this.sections = sections.map((section) => {

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreFilepoolProvider } from '../../../providers/filepool';
import { CoreSitesProvider } from '../../../providers/sites';
@ -21,10 +22,13 @@ import { CoreTextUtilsProvider } from '../../../providers/utils/text';
import { CoreTimeUtilsProvider } from '../../../providers/utils/time';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreCoursesDelegate, CoreCoursesHandlerToDisplay } from '../../courses/providers/delegate';
import { CoreSiteHomeProvider } from '../../sitehome/providers/sitehome';
import { CoreCourseProvider } from './course';
import { CoreCourseModuleDelegate } from './module-delegate';
import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from './module-prefetch-delegate';
import { CoreLoginHelperProvider } from '../../login/providers/helper';
import { CoreConstants } from '../../constants';
import { CoreSite } from '../../../classes/site';
import * as moment from 'moment';
/**
@ -109,7 +113,8 @@ export class CoreCourseHelperProvider {
private moduleDelegate: CoreCourseModuleDelegate, private prefetchDelegate: CoreCourseModulePrefetchDelegate,
private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider,
private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
private utils: CoreUtilsProvider, private translate: TranslateService, private coursesDelegate: CoreCoursesDelegate) {}
private utils: CoreUtilsProvider, private translate: TranslateService, private coursesDelegate: CoreCoursesDelegate,
private loginHelper: CoreLoginHelperProvider, private siteHomeProvider: CoreSiteHomeProvider) {}
/**
* This function treats every module on the sections provided to load the handler data, treat completion
@ -117,11 +122,10 @@ export class CoreCourseHelperProvider {
*
* @param {any[]} sections List of sections to treat modules.
* @param {number} courseId Course ID of the modules.
* @param {number} [moduleId] Module to navigate to if needed.
* @param {any[]} [completionStatus] List of completion status.
* @return {boolean} Whether the sections have content.
*/
addHandlerDataForModules(sections: any[], courseId: number, moduleId?: number, completionStatus?: any) {
addHandlerDataForModules(sections: any[], courseId: number, completionStatus?: any) {
let hasContent = false;
sections.forEach((section) => {
@ -139,11 +143,6 @@ export class CoreCourseHelperProvider {
module.completionstatus = completionStatus[module.id];
module.completionstatus.courseId = courseId;
}
if (module.id == moduleId) {
// This is the module we're looking for. Open it.
module.handlerData.action(new Event('click'), module, courseId);
}
});
});
@ -578,6 +577,86 @@ export class CoreCourseHelperProvider {
return 'Section-' + section.id;
}
/**
* Navigate to a module.
*
* @param {number} moduleId Module's ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [courseId] Course ID. If not defined we'll try to retrieve it from the site.
* @param {number} [sectionId] Section the module belongs to. If not defined we'll try to retrieve it from the site.
* @return {Promise<void>} Promise resolved when done.
*/
navigateToModule(moduleId: number, siteId?: string, courseId?: number, sectionId?: number) : Promise<void> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
let modal = this.domUtils.showModalLoading(),
promise,
site: CoreSite;
if (courseId && sectionId) {
// No need to retrieve more data.
promise = Promise.resolve();
} else if (!courseId) {
// We don't have courseId.
promise = this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => {
courseId = module.course;
sectionId = module.section;
});
} else {
// We don't have sectionId but we have courseId.
promise = this.courseProvider.getModuleSectionId(moduleId, siteId).then((id) => {
sectionId = id;
});
}
return promise.then(() => {
// Get the site.
return this.sitesProvider.getSite(siteId);
}).then((s) => {
site = s;
// Get the module.
return this.courseProvider.getModule(moduleId, courseId, sectionId, false, false, siteId);
}).then((module) => {
const params = {
course: {id: courseId},
module: module,
sectionId: sectionId
};
module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId);
if (courseId == site.getSiteHomeId()) {
// Check if site home is available.
return this.siteHomeProvider.isAvailable().then(() => {
this.loginHelper.redirect('CoreSiteHomeIndexPage', params, siteId);
});
} else {
this.loginHelper.redirect('CoreCourseSectionPage', params, siteId);
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
}).finally(() => {
modal.dismiss();
});
}
/**
* Open a module.
*
* @param {NavController} navCtrl The NavController to use.
* @param {any} module The module to open.
* @param {number} courseId The course ID of the module.
* @param {number} [sectionId] The section ID of the module.
*/
openModule(navCtrl: NavController, module: any, courseId: number, sectionId?: number) : void {
if (!module.handlerData) {
module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId);
}
module.handlerData.action(new Event('click'), navCtrl, module, courseId, {animate: false});
}
/**
* Prefetch all the activities in a course and also the course addons.
*

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { NavController } from 'ionic-angular';
import { NavController, NavOptions } from 'ionic-angular';
import { CoreEventsProvider } from '../../../providers/events';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
@ -104,8 +104,9 @@ export interface CoreCourseModuleHandlerData {
* @param {NavController} navCtrl NavController instance.
* @param {any} module The module object.
* @param {number} courseId The course ID.
* @param {NavOptions} [options] Options for the navigation.
*/
action?(event: Event, navCtrl: NavController, module: any, courseId: number) : void;
action?(event: Event, navCtrl: NavController, module: any, courseId: number, options?: NavOptions) : void;
};
/**
@ -208,11 +209,11 @@ export class CoreCourseModuleDelegate {
icon: this.courseProvider.getModuleIconSrc(module.modname),
title: module.name,
class: 'core-course-default-handler core-course-module-' + module.modname + '-handler',
action: (event: Event, navCtrl: NavController, module: any, courseId: number) => {
action: (event: Event, navCtrl: NavController, module: any, courseId: number, options?: NavOptions) => {
event.preventDefault();
event.stopPropagation();
navCtrl.push('CoreCourseUnsupportedModulePage', {module: module});
navCtrl.push('CoreCourseUnsupportedModulePage', {module: module}, options);
}
};

View File

@ -13,11 +13,13 @@
// limitations under the License.
import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
import { CoreUtilsProvider } from '../../../../providers/utils/utils';
import { CoreCourseProvider } from '../../../course/providers/course';
import { CoreContentLinksHelperProvider } from '../../../contentlinks/providers/helper';
import * as moment from 'moment';
/**
@ -41,9 +43,9 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges {
next30Days: any[] = [];
future: any[] = [];
constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider,
constructor(private navCtrl: NavController, private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider,
private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider,
private courseProvider: CoreCourseProvider) {
private courseProvider: CoreCourseProvider, private contentLinksHelper: CoreContentLinksHelperProvider) {
this.loadMore = new EventEmitter();
}
@ -100,9 +102,6 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges {
loadMoreEvents() {
this.loadingMore = true;
this.loadMore.emit();
// this.loadMore().finally(function() {
// scope.loadingMore = false;
// });
}
/**
@ -119,19 +118,14 @@ export class CoreCoursesOverviewEventsComponent implements OnChanges {
url = this.textUtils.decodeHTMLEntities(url);
let modal = this.domUtils.showModalLoading();
this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url).finally(() => {
this.contentLinksHelper.handleLink(url, undefined, this.navCtrl).then((treated) => {
if (!treated) {
return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
}
}).finally(() => {
modal.dismiss();
});
// @todo
// $mmContentLinksHelper.handleLink(url).then((treated) => {
// if (!treated) {
// return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
// }
// }).finally(() => {
// modal.dismiss();
// });
return false;
}
}

View File

@ -14,10 +14,14 @@
import { NgModule } from '@angular/core';
import { CoreCoursesProvider } from './providers/courses';
import { CoreCoursesMainMenuHandler } from './providers/handlers';
import { CoreCoursesMainMenuHandler } from './providers/mainmenu-handler';
import { CoreCoursesMyOverviewProvider } from './providers/my-overview';
import { CoreCoursesDelegate } from './providers/delegate';
import { CoreCoursesCourseLinkHandler } from './providers/course-link-handler';
import { CoreCoursesIndexLinkHandler } from './providers/courses-index-link-handler';
import { CoreCoursesMyOverviewLinkHandler } from './providers/my-overview-link-handler';
import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate';
@NgModule({
declarations: [],
@ -27,12 +31,21 @@ import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
CoreCoursesProvider,
CoreCoursesMainMenuHandler,
CoreCoursesMyOverviewProvider,
CoreCoursesDelegate
CoreCoursesDelegate,
CoreCoursesCourseLinkHandler,
CoreCoursesIndexLinkHandler,
CoreCoursesMyOverviewLinkHandler
],
exports: []
})
export class CoreCoursesModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreCoursesMainMenuHandler) {
constructor(mainMenuDelegate: CoreMainMenuDelegate, contentLinksDelegate: CoreContentLinksDelegate,
mainMenuHandler: CoreCoursesMainMenuHandler, courseLinkHandler: CoreCoursesCourseLinkHandler,
indexLinkHandler: CoreCoursesIndexLinkHandler, myOverviewLinkHandler: CoreCoursesMyOverviewLinkHandler) {
mainMenuDelegate.registerHandler(mainMenuHandler);
contentLinksDelegate.registerHandler(courseLinkHandler);
contentLinksDelegate.registerHandler(indexLinkHandler);
contentLinksDelegate.registerHandler(myOverviewLinkHandler);
}
}

View File

@ -23,7 +23,6 @@
<ion-icon name="folder" item-start></ion-icon>
<h2><core-format-text [text]="category.name"></core-format-text></h2>
<ion-badge item-end *ngIf="category.coursecount > 0" color="light">{{category.coursecount}}</ion-badge>
<ion-icon item-end name="arrow-forward" md="ios-arrow-forward" class="icon-accessory"></ion-icon>
</a>
</section>
</div>

View File

@ -19,11 +19,19 @@
</ion-refresher>
<core-loading [hideUntil]="coursesLoaded">
<ion-item *ngIf="showFilter" class="item-transparent">
<ion-icon name="funnel" class="placeholder-icon" item-start></ion-icon>
<ion-input type="text" name="filter" placeholder="{{ 'core.courses.filtermycourses' | translate }}" [(ngModel)]="filter" (ngModelChange)="filterChanged($event)"></ion-input>
<div no-padding padding-bottom *ngIf="showFilter">
<ion-item class="item-transparent">
<ion-label item-start><ion-icon name="funnel" class="placeholder-icon"></ion-icon></ion-label>
<ion-input type="text" name="filter" clearInput [(ngModel)]="filter" (ngModelChange)="filterChanged($event)" [placeholder]="'core.courses.filtermycourses' | translate"></ion-input>
</ion-item>
<core-courses-course-progress *ngFor="let course of filteredCourses" [course]="course" showSummary="true"></core-courses-course-progress>
</div>
<ion-grid no-padding>
<ion-row no-padding>
<ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-4 align-self-stretch>
<core-courses-course-progress [course]="course" class="core-courseoverview"></core-courses-course-progress>
</ion-col>
</ion-row>
</ion-grid>
<core-empty-box *ngIf="!courses || !courses.length" icon="ionic" [message]="'core.courses.nocourses' | translate">
<p *ngIf="searchEnabled">{{ 'core.courses.searchcoursesadvice' | translate }}</p>
</core-empty-box>

View File

@ -12,16 +12,25 @@
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<core-tabs [selectedIndex]="firstSelectedTab" [hideUntil]="tabsReady">
<!-- Site home tab. -->
<core-tab [show]="siteHomeEnabled" [title]="'core.sitehome.sitehome' | translate" (ionSelect)="tabChanged('sitehome')">
<ng-template>
<core-sitehome-index></core-sitehome-index>
</ng-template>
</core-tab>
<!-- Timeline tab. -->
<core-tab [title]="'core.courses.timeline' | translate" (ionSelect)="tabChanged('timeline')">
<ng-template>
<ion-content>
<ion-refresher [enabled]="timeline.loaded || timelineCourses.loaded || courses.loaded" (ionRefresh)="refreshMyOverview($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-tabs selectedIndex="1">
<!-- Timeline tab. -->
<core-tab [title]="'core.courses.timeline' | translate" (ionSelect)="tabChanged('timeline')">
<div no-padding [hidden]="!(timeline.loaded || timelineCourses.loaded)">
<ion-select [(ngModel)]="timeline.sort" (ngModelChange)="switchSort()">
<ion-select [(ngModel)]="timeline.sort" (ngModelChange)="switchSort()" interface="popover">
<ion-option value="sortbydates">{{ 'core.courses.sortbydates' | translate }}</ion-option>
<ion-option value="sortbycourses">{{ 'core.courses.sortbycourses' | translate }}</ion-option>
</ion-select>
@ -41,14 +50,22 @@
</ion-grid>
<core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursesoverview' | translate"></core-empty-box>
</core-loading>
</ion-content>
</ng-template>
</core-tab>
<!-- Courses tab. -->
<core-tab [title]="'core.courses.courses' | translate" (ionSelect)="tabChanged('courses')">
<ng-template>
<ion-content>
<ion-refresher [enabled]="timeline.loaded || timelineCourses.loaded || courses.loaded" (ionRefresh)="refreshMyOverview($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="courses.loaded" class="core-loading-center">
<!-- "Time" selector. -->
<div no-padding class="clearfix" [hidden]="showFilter">
<ion-select [title]="'core.show' | translate" [(ngModel)]="courses.selected" float-start (ngModelChange)="selectedChanged()">
<ion-select [title]="'core.show' | translate" [(ngModel)]="courses.selected" float-start (ngModelChange)="selectedChanged()" interface="popover">
<ion-option value="inprogress">{{ 'core.courses.inprogress' | translate }}</ion-option>
<ion-option value="future">{{ 'core.courses.future' | translate }}</ion-option>
<ion-option value="past">{{ 'core.courses.past' | translate }}</ion-option>
@ -65,7 +82,7 @@
<!-- Filter courses. -->
<div no-padding padding-bottom [hidden]="!showFilter">
<ion-item>
<ion-label><ion-icon name="funnel" class="placeholder-icon"></ion-icon></ion-label>
<ion-label item-start><ion-icon name="funnel" class="placeholder-icon"></ion-icon></ion-label>
<ion-input type="text" name="filter" clearInput [(ngModel)]="courses.filter" (ngModelChange)="filterChanged($event)" [placeholder]="'core.courses.filtermycourses' | translate"></ion-input>
</ion-item>
</div>
@ -84,7 +101,8 @@
<core-empty-box *ngIf="courses[courses.selected].length == 0 && courses.selected == 'past'" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursespast' | translate"></core-empty-box>
</div>
</core-loading>
</ion-content>
</ng-template>
</core-tab>
</core-tabs>
</ion-content>

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreCoursesMyOverviewPage } from './my-overview';
import { CoreComponentsModule } from '../../../../components/components.module';
import { CoreCoursesComponentsModule } from '../../components/components.module';
import { CoreSiteHomeComponentsModule } from '../../../sitehome/components/components.module';
@NgModule({
declarations: [
@ -26,6 +27,7 @@ import { CoreCoursesComponentsModule } from '../../components/components.module'
imports: [
CoreComponentsModule,
CoreCoursesComponentsModule,
CoreSiteHomeComponentsModule,
IonicPageModule.forChild(CoreCoursesMyOverviewPage),
TranslateModule.forChild()
],

View File

@ -14,10 +14,13 @@
import { Component, OnDestroy } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreCoursesProvider } from '../../providers/courses';
import { CoreCoursesDelegate } from '../../providers/delegate';
import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview';
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
import { CoreSiteHomeProvider } from '../../../sitehome/providers/sitehome';
import * as moment from 'moment';
/**
@ -29,6 +32,9 @@ import * as moment from 'moment';
templateUrl: 'my-overview.html',
})
export class CoreCoursesMyOverviewPage implements OnDestroy {
firstSelectedTab: number;
siteHomeEnabled: boolean;
tabsReady: boolean = false;
tabShown = 'courses';
timeline = {
sort: 'sortbydates',
@ -64,13 +70,24 @@ export class CoreCoursesMyOverviewPage implements OnDestroy {
constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider,
private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider,
private courseHelper: CoreCourseHelperProvider) {}
private courseHelper: CoreCourseHelperProvider, private sitesProvider: CoreSitesProvider,
private siteHomeProvider: CoreSiteHomeProvider, private coursesDelegate: CoreCoursesDelegate) {}
/**
* View loaded.
*/
ionViewDidLoad() {
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
// Decide which tab to load first.
this.siteHomeProvider.isAvailable().then((enabled) => {
let site = this.sitesProvider.getCurrentSite(),
displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0;
this.siteHomeEnabled = enabled;
this.firstSelectedTab = displaySiteHome ? 0 : 2;
this.tabsReady = true;
});
}
/**
@ -222,7 +239,7 @@ export class CoreCoursesMyOverviewPage implements OnDestroy {
}
promises.push(this.coursesProvider.invalidateUserCourses());
// promises.push(this.coursesDelegate.clearAndInvalidateCoursesOptions());
promises.push(this.coursesDelegate.clearAndInvalidateCoursesOptions());
return Promise.all(promises).finally(() => {
switch (this.tabShown) {

View File

@ -0,0 +1,267 @@
// (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 { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '../../contentlinks/providers/delegate';
import { CoreLoginHelperProvider } from '../../login/providers/helper';
import { CoreCourseProvider } from '../../course/providers/course';
import { CoreCoursesProvider } from './courses';
/**
* Handler to treat links to course view or enrol (except site home).
*/
@Injectable()
export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase {
name = 'CoreCoursesCourseLinkHandler';
pattern = /((\/enrol\/index\.php)|(\/course\/enrol\.php)|(\/course\/view\.php)).*([\?\&]id=\d+)/;
protected waitStart = 0;
constructor(private sitesProvider: CoreSitesProvider, private coursesProvider: CoreCoursesProvider,
private loginHelper: CoreLoginHelperProvider, private domUtils: CoreDomUtilsProvider,
private translate: TranslateService, private courseProvider: CoreCourseProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
courseId = parseInt(params.id, 10);
let sectionId = params.sectionid ? parseInt(params.sectionid, 10) : null,
sectionNumber = typeof params.section != 'undefined' ? parseInt(params.section, 10) : NaN,
pageParams: any = {
course: {id: courseId},
sectionId: sectionId || null
};
if (!isNaN(sectionNumber)) {
pageParams.sectionNumber = sectionNumber;
}
return [{
action: (siteId, navCtrl?) => {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (siteId == this.sitesProvider.getCurrentSiteId()) {
this.actionEnrol(courseId, url, pageParams).catch(() => {
// Ignore errors.
});
} else {
// Use redirect to make the course the new history root (to avoid "loops" in history).
this.loginHelper.redirect('CoreCourseSectionPage', pageParams, siteId);
}
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise<boolean> {
courseId = parseInt(params.id, 10);
if (!courseId) {
return false;
}
// Get the course id of Site Home.
return this.sitesProvider.getSiteHomeId(siteId).then((siteHomeId) => {
return courseId != siteHomeId;
});
}
/**
* Action to perform when an enrol link is clicked.
*
* @param {number} courseId Course ID.
* @param {string} url Treated URL.
* @param {any} pageParams Params to send to the new page.
* @return {Promise<any>} Promise resolved when done.
*/
protected actionEnrol(courseId: number, url: string, pageParams: any) : Promise<any> {
let modal = this.domUtils.showModalLoading(),
isEnrolUrl = !!url.match(/(\/enrol\/index\.php)|(\/course\/enrol\.php)/);
// Check if user is enrolled in the course.
return this.coursesProvider.getUserCourse(courseId).catch(() => {
// User is not enrolled in the course. Check if can self enrol.
return this.canSelfEnrol(courseId).then(() => {
modal.dismiss();
// The user can self enrol. If it's not a enrolment URL we'll ask for confirmation.
let promise = isEnrolUrl ? Promise.resolve() :
this.domUtils.showConfirm(this.translate.instant('core.courses.confirmselfenrol'));
return promise.then(() => {
// Enrol URL or user confirmed.
return this.selfEnrol(courseId).catch((error) => {
if (error) {
this.domUtils.showErrorModal(error);
}
return Promise.reject(null);
});
}, () => {
// User cancelled. Check if the user can view the course contents (guest access or similar).
return this.courseProvider.getSections(courseId, false, true);
});
}, (error) => {
// Can't self enrol. Check if the user can view the course contents (guest access or similar).
return this.courseProvider.getSections(courseId, false, true).catch(() => {
// Error. Show error message and allow the user to open the link in browser.
modal.dismiss();
if (error) {
error = error.message || error.error || error.content || error.body || error;
}
if (!error) {
error = this.translate.instant('core.courses.notenroled');
}
let body = this.translate.instant('core.twoparagraphs',
{p1: error, p2: this.translate.instant('core.confirmopeninbrowser')});
this.domUtils.showConfirm(body).then(() => {
this.sitesProvider.getCurrentSite().openInBrowserWithAutoLogin(url);
}).catch(() => {
// User cancelled.
});
return Promise.reject(null);
});
});
}).then(() => {
modal.dismiss();
// Use redirect to make the course the new history root (to avoid "loops" in history).
this.loginHelper.redirect('CoreCourseSectionPage', pageParams, this.sitesProvider.getCurrentSiteId());
});
}
/**
* Check if a user can be "automatically" self enrolled in a course.
*
* @param {number} courseId Course ID.
* @return {Promise<any>} Promise resolved if user can be enrolled in a course, rejected otherwise.
*/
protected canSelfEnrol(courseId: number) : Promise<any> {
// Check that the course has self enrolment enabled.
return this.coursesProvider.getCourseEnrolmentMethods(courseId).then((methods) => {
let isSelfEnrolEnabled = false,
instances = 0;
methods.forEach((method) => {
if (method.type == 'self' && method.status) {
isSelfEnrolEnabled = true;
instances++;
}
});
if (!isSelfEnrolEnabled || instances != 1) {
// Self enrol not enabled or more than one instance.
return Promise.reject(null);
}
});
}
/**
* Try to self enrol a user in a course.
*
* @param {number} courseId Course ID.
* @param {string} [password] Password.
* @return {Promise<any>} Promise resolved when the user is enrolled, rejected otherwise.
*/
protected selfEnrol(courseId: number, password?: string) : Promise<any> {
const modal = this.domUtils.showModalLoading();
return this.coursesProvider.selfEnrol(courseId, password).then(() => {
// Success self enrolling the user, invalidate the courses list.
return this.coursesProvider.invalidateUserCourses().catch(() => {
// Ignore errors.
}).then(() => {
// Sometimes the list of enrolled courses takes a while to be updated. Wait for it.
return this.waitForEnrolled(courseId, true).finally(() => {
modal.dismiss();
});
});
}).catch((error) => {
modal.dismiss();
if (error && error.code === CoreCoursesProvider.ENROL_INVALID_KEY) {
// Invalid password. Allow the user to input password.
let title = this.translate.instant('core.courses.selfenrolment'),
body = ' ', // Empty message.
placeholder = this.translate.instant('core.courses.password');
if (typeof password != 'undefined') {
// The user attempted a password. Show an error message.
this.domUtils.showErrorModal(error.message);
}
return this.domUtils.showPrompt(body, title, placeholder).then((password) => {
return this.selfEnrol(courseId, password);
});
} else {
return Promise.reject(error);
}
});
}
/**
* Wait for the user to be enrolled in a course.
*
* @param {number} courseId The course ID.
* @param {boolean} first If it's the first call (true) or it's a recursive call (false).
* @return {Promise<any>} Promise resolved when enrolled or timeout.
*/
protected waitForEnrolled(courseId: number, first?: boolean) : Promise<any> {
if (first) {
this.waitStart = Date.now();
}
// Check if user is enrolled in the course.
return this.coursesProvider.invalidateUserCourses().catch(() => {
// Ignore errors.
}).then(() => {
return this.coursesProvider.getUserCourse(courseId);
}).catch(() => {
// Not enrolled, wait a bit and try again.
if (Date.now() - this.waitStart > 60000) {
// Max time reached, stop.
return;
}
return new Promise((resolve, reject) => {
setTimeout(() => {
this.waitForEnrolled(courseId).then(resolve);
}, 5000);
});
});
}
}

View File

@ -0,0 +1,64 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '../../contentlinks/providers/delegate';
import { CoreLoginHelperProvider } from '../../login/providers/helper';
import { CoreCoursesProvider } from './courses';
/**
* Handler to treat links to course index (list of courses).
*/
@Injectable()
export class CoreCoursesIndexLinkHandler extends CoreContentLinksHandlerBase {
name = 'CoreCoursesIndexLinkHandler';
featureName = '$mmSideMenuDelegate_mmCourses';
pattern = /\/course\/?(index\.php.*)?$/;
constructor(private coursesProvider: CoreCoursesProvider, private loginHelper: CoreLoginHelperProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?) => {
var page = 'CoreCoursesMyCoursesPage', // By default, go to My Courses.
pageParams: any = {};
if (this.coursesProvider.isGetCoursesByFieldAvailable()) {
if (params.categoryid) {
page = 'CoreCoursesCategoriesPage';
pageParams.categoryId = parseInt(params.categoryid, 10);
} else {
page = 'CoreCoursesAvailableCoursesPage';
}
}
// Always use redirect to make it the new history root (to avoid "loops" in history).
this.loginHelper.redirect(page, pageParams, siteId);
}
}];
}
}

View File

@ -18,7 +18,7 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/pro
import { CoreCoursesMyOverviewProvider } from '../providers/my-overview';
/**
* Handler to inject an option into main menu.
* Handler to add My Courses or My Overview into main menu.
*/
@Injectable()
export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler {

View File

@ -0,0 +1,52 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '../../contentlinks/providers/delegate';
import { CoreLoginHelperProvider } from '../../login/providers/helper';
import { CoreCoursesProvider } from './courses';
/**
* Handler to treat links to my overview.
*/
@Injectable()
export class CoreCoursesMyOverviewLinkHandler extends CoreContentLinksHandlerBase {
name = 'CoreCoursesMyOverviewLinkHandler';
featureName = '$mmSideMenuDelegate_mmCourses';
pattern = /\/my\/?$/;
constructor(private coursesProvider: CoreCoursesProvider, private loginHelper: CoreLoginHelperProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?) => {
// Always use redirect to make it the new history root (to avoid "loops" in history).
this.loginHelper.redirect('CoreCoursesMyOverviewPage', undefined, siteId);
}
}];
}
}

View File

@ -21,6 +21,8 @@ import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreUtilsProvider } from '../../../../providers/utils/utils';
import { CoreLoginHelperProvider } from '../../providers/helper';
import { CoreContentLinksDelegate } from '../../../contentlinks/providers/delegate';
import { CoreContentLinksHelperProvider } from '../../../contentlinks/providers/helper';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
/**
@ -52,7 +54,8 @@ export class CoreLoginCredentialsPage {
constructor(private navCtrl: NavController, navParams: NavParams, fb: FormBuilder, private appProvider: CoreAppProvider,
private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider,
private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
private eventsProvider: CoreEventsProvider) {
private eventsProvider: CoreEventsProvider, private contentLinksDelegate: CoreContentLinksDelegate,
private contentLinksHelper: CoreContentLinksHelperProvider) {
this.siteUrl = navParams.get('siteUrl');
this.siteConfig = navParams.get('siteConfig');
@ -203,16 +206,15 @@ export class CoreLoginCredentialsPage {
if (this.urlToOpen) {
// There's a content link to open.
// @todo: Implement this once content links delegate is implemented.
// return $mmContentLinksDelegate.getActionsFor(urlToOpen, undefined, username).then((actions) => {
// action = $mmContentLinksHelper.getFirstValidAction(actions);
// if (action && action.sites.length) {
// // Action should only have 1 site because we're filtering by username.
// action.action(action.sites[0]);
// } else {
// return $mmLoginHelper.goToSiteInitialPage();
// }
// });
return this.contentLinksDelegate.getActionsFor(this.urlToOpen, undefined, username).then((actions) => {
const action = this.contentLinksHelper.getFirstValidAction(actions);
if (action && action.sites.length) {
// Action should only have 1 site because we're filtering by username.
action.action(action.sites[0]);
} else {
return this.loginHelper.goToSiteInitialPage();
}
});
} else {
return this.loginHelper.goToSiteInitialPage();
}

View File

@ -69,7 +69,7 @@
</ion-item>
<ion-item text-wrap>
<ion-label stacked id="core-login-signup-country">{{ 'core.user.country' | translate }}</ion-label>
<ion-select name="country" formControlName="country" aria-labelledby="core-login-signup-country">
<ion-select name="country" formControlName="country" aria-labelledby="core-login-signup-country" interface="popover">
<ion-option value="">{{ 'core.login.selectacountry' | translate }}</ion-option>
<ion-option *ngFor="let key of countriesKeys" [value]="key">{{countries[key]}}</ion-option>
</ion-select>

View File

@ -29,7 +29,7 @@
<ion-item *ngIf="!displayAsButtons">
<!-- @todo: Display label and select in different lines. -->
<ion-label for="siteSelect">{{ 'core.login.selectsite' | translate }}</ion-label>
<ion-select formControlName="siteUrl" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}">
<ion-select formControlName="siteUrl" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" interface="popover">
<ion-option *ngFor="let site of fixedSites" [value]="site.url">{{site.name}}</ion-option>
</ion-select>
</ion-item>

View File

@ -404,49 +404,6 @@ export class CoreLoginHelperProvider {
*/
goToSiteInitialPage() : Promise<any> {
return this.appProvider.getRootNavController().setRoot('CoreMainMenuPage');
// return this.isMyOverviewEnabled().then((myOverview) => {
// let myCourses = !myOverview && this.isMyCoursesEnabled(),
// site = this.sitesProvider.getCurrentSite(),
// promise;
// if (!site) {
// return Promise.reject(null);
// }
// // Check if frontpage is needed to be shown. (If configured or if any of the other avalaible).
// if ((site.getInfo() && site.getInfo().userhomepage === 0) || (!myCourses && !myOverview)) {
// promise = this.isFrontpageEnabled();
// } else {
// promise = Promise.resolve(false);
// }
// return promise.then((frontpage) => {
// // Check avalaibility in priority order.
// let pageName,
// params;
// // @todo Use real pages names when they are implemented.
// if (frontpage) {
// pageName = 'Frontpage';
// } else if (myOverview) {
// pageName = 'MyOverview';
// } else if (myCourses) {
// pageName = 'MyCourses';
// } else {
// // Anything else available, go to the user profile.
// pageName = 'User';
// params = {
// userId: site.getUserId()
// };
// }
// if (setRoot) {
// return navCtrl.setRoot(pageName, params, {animate: false});
// } else {
// return navCtrl.push(pageName, params);
// }
// });
// });
}
/**
@ -547,45 +504,6 @@ export class CoreLoginHelperProvider {
return !!CoreConfigConstants.siteurl;
}
/**
* Check if the app is configured to use a fixed URL (only 1).
*
* @return {Promise<boolean>} Promise resolved with boolean: whether there is 1 fixed URL.
*/
protected isFrontpageEnabled() : Promise<boolean> {
// var $mmaFrontpage = $mmAddonManager.get('$mmaFrontpage');
// if ($mmaFrontpage && !$mmaFrontpage.isDisabledInSite()) {
// return $mmaFrontpage.isFrontpageAvailable().then(() => {
// return true;
// }).catch(() => {
// return false;
// });
// }
// @todo: Implement it when front page is implemented.
return Promise.resolve(false);
}
/**
* Check if My Courses is enabled.
*
* @return {boolean} Whether My Courses is enabled.
*/
protected isMyCoursesEnabled() : boolean {
// @todo: Implement it when My Courses is implemented.
return false;
// return !$mmCourses.isMyCoursesDisabledInSite();
}
/**
* Check if My Overview is enabled.
*
* @return {Promise<boolean>} Promise resolved with boolean: whether My Overview is enabled.
*/
protected isMyOverviewEnabled() : Promise<boolean> {
// @todo: Implement it when My Overview is implemented.
return Promise.resolve(false);
}
/**
* Check if current site is logged out, triggering mmCoreEventSessionExpired if it is.
*
@ -858,7 +776,7 @@ export class CoreLoginHelperProvider {
if (siteId) {
this.loadSiteAndPage(page, params, siteId);
} else {
this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage')
this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage');
}
}
}

View File

@ -1,4 +1,4 @@
<ion-tabs *ngIf="loaded" #mainTabs>
<ion-tabs *ngIf="loaded" #mainTabs [selectedIndex]="initialTab">
<ion-tab [enabled]="false" [show]="false" [root]="redirectPage" [rootParams]="redirectParams"></ion-tab>
<ion-tab *ngFor="let tab of tabs" [root]="tab.page" [tabTitle]="tab.title | translate" [tabIcon]="tab.icon" class="{{tab.class}}"></ion-tab>
</ion-tabs>

View File

@ -54,6 +54,7 @@ export class CoreMainMenuPage implements OnDestroy {
loaded: boolean;
redirectPage: string;
redirectParams: any;
initialTab: number;
protected subscription;
protected moreTabData = {
@ -79,25 +80,36 @@ export class CoreMainMenuPage implements OnDestroy {
return;
}
let site = this.sitesProvider.getCurrentSite(),
displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0;
this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => {
handlers = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers.
// Check if handlers are already in tabs. Add the ones that aren't.
// @todo: https://github.com/ionic-team/ionic/issues/13633
for (let i in handlers) {
for (let i = 0; i < handlers.length; i++) {
let handler = handlers[i],
found = false;
found = false,
shouldSelect = (displaySiteHome && handler.name == 'CoreSiteHome') ||
(!displaySiteHome && handler.name == 'CoreCourses');
for (let j in this.tabs) {
for (let j = 0; j < this.tabs.length; j++) {
let tab = this.tabs[j];
if (tab.title == handler.title && tab.icon == handler.icon) {
found = true;
if (shouldSelect) {
this.initialTab = j;
}
break;
}
}
if (!found) {
this.tabs.push(handler);
if (shouldSelect) {
this.initialTab = this.tabs.length;
}
}
}

View File

@ -79,6 +79,18 @@ export interface CoreMainMenuHandlerData {
class?: string;
};
/**
* Data returned by the delegate for each handler.
*/
export interface CoreMainMenuHandlerToDisplay extends CoreMainMenuHandlerData {
/**
* Name of the handler.
* @type {string}
*/
name?: string;
};
/**
* Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin
* and notify an update in the data.
@ -90,7 +102,7 @@ export class CoreMainMenuDelegate {
protected enabledHandlers: {[s: string]: CoreMainMenuHandler} = {};
protected loaded = false;
protected lastUpdateHandlersStart: number;
protected siteHandlers: Subject<CoreMainMenuHandlerData[]> = new BehaviorSubject<CoreMainMenuHandlerData[]>([]);
protected siteHandlers: Subject<CoreMainMenuHandlerToDisplay[]> = new BehaviorSubject<CoreMainMenuHandlerToDisplay[]>([]);
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) {
this.logger = logger.getInstance('CoreMainMenuDelegate');
@ -123,7 +135,7 @@ export class CoreMainMenuDelegate {
*
* @return {Subject<CoreMainMenuHandlerData[]>} An observable that will receive the handlers.
*/
getHandlers() : Subject<CoreMainMenuHandlerData[]> {
getHandlers() : Subject<CoreMainMenuHandlerToDisplay[]> {
return this.siteHandlers;
}
@ -223,7 +235,9 @@ export class CoreMainMenuDelegate {
for (let name in this.enabledHandlers) {
let handler = this.enabledHandlers[name],
data = handler.getDisplayData();
data: CoreMainMenuHandlerToDisplay = handler.getDisplayData();
data.name = handler.name;
handlersData.push({
data: data,

View File

@ -0,0 +1,4 @@
<a *ngIf="show" ion-item text-wrap [navPush]="'CoreCoursesAvailableCoursesPage'">
<ion-icon name="ionic" item-start></ion-icon>
<h2>{{ 'core.courses.availablecourses' | translate}}</h2>
</a>

View File

@ -0,0 +1,32 @@
// (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 } from '@angular/core';
import { IonicPage } from 'ionic-angular';
import { CoreCoursesProvider } from '../../../courses/providers/courses';
/**
* Component to open the page to view the list of all courses.
*/
@Component({
selector: 'core-sitehome-all-course-list',
templateUrl: 'all-course-list.html',
})
export class CoreSiteHomeAllCourseListComponent {
show: boolean;
constructor(coursesProvider: CoreCoursesProvider) {
this.show = coursesProvider.isGetCoursesByFieldAvailable();
}
}

View File

@ -0,0 +1,4 @@
<a *ngIf="show" ion-item text-wrap [navPush]="'CoreCoursesCategoriesPage'">
<ion-icon name="folder" item-start></ion-icon>
<h2>{{ 'core.courses.categories' | translate}}</h2>
</a>

View File

@ -0,0 +1,32 @@
// (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 } from '@angular/core';
import { IonicPage } from 'ionic-angular';
import { CoreCoursesProvider } from '../../../courses/providers/courses';
/**
* Component to open the page to view the list of categories.
*/
@Component({
selector: 'core-sitehome-categories',
templateUrl: 'categories.html',
})
export class CoreSiteHomeCategoriesComponent {
show: boolean;
constructor(coursesProvider: CoreCoursesProvider) {
this.show = coursesProvider.isGetCoursesByFieldAvailable();
}
}

View File

@ -0,0 +1,55 @@
// (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 { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../../components/components.module';
import { CoreDirectivesModule } from '../../../directives/directives.module';
import { CoreCourseComponentsModule } from '../../course/components/components.module';
import { CoreSiteHomeIndexComponent } from './index/index';
import { CoreSiteHomeAllCourseListComponent } from './all-course-list/all-course-list';
import { CoreSiteHomeCategoriesComponent } from './categories/categories';
import { CoreSiteHomeCourseSearchComponent } from './course-search/course-search';
import { CoreSiteHomeEnrolledCourseListComponent } from './enrolled-course-list/enrolled-course-list';
import { CoreSiteHomeNewsComponent } from './news/news';
@NgModule({
declarations: [
CoreSiteHomeIndexComponent,
CoreSiteHomeAllCourseListComponent,
CoreSiteHomeCategoriesComponent,
CoreSiteHomeCourseSearchComponent,
CoreSiteHomeEnrolledCourseListComponent,
CoreSiteHomeNewsComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCourseComponentsModule
],
exports: [
CoreSiteHomeIndexComponent,
CoreSiteHomeAllCourseListComponent,
CoreSiteHomeCategoriesComponent,
CoreSiteHomeCourseSearchComponent,
CoreSiteHomeEnrolledCourseListComponent,
CoreSiteHomeNewsComponent
]
})
export class CoreSiteHomeComponentsModule {}

View File

@ -0,0 +1,4 @@
<a *ngIf="show" ion-item text-wrap [navPush]="'CoreCoursesSearchPage'">
<ion-icon name="search" item-start></ion-icon>
<h2>{{ 'core.courses.searchcourses' | translate}}</h2>
</a>

View File

@ -0,0 +1,32 @@
// (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 } from '@angular/core';
import { IonicPage } from 'ionic-angular';
import { CoreCoursesProvider } from '../../../courses/providers/courses';
/**
* Component to open the page to search courses.
*/
@Component({
selector: 'core-sitehome-course-search',
templateUrl: 'course-search.html',
})
export class CoreSiteHomeCourseSearchComponent {
show: boolean;
constructor(coursesProvider: CoreCoursesProvider) {
this.show = !coursesProvider.isSearchCoursesDisabledInSite();
}
}

View File

@ -0,0 +1,4 @@
<a *ngIf="show" ion-item text-wrap [navPush]="'CoreCoursesMyCoursesPage'">
<ion-icon name="ionic" item-start></ion-icon>
<h2>{{ 'core.courses.mycourses' | translate}}</h2>
</a>

View File

@ -0,0 +1,43 @@
// (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 { IonicPage } from 'ionic-angular';
import { CoreCoursesProvider } from '../../../courses/providers/courses';
/**
* Component to open the page to view the list of courses the user is enrolled in.
*/
@Component({
selector: 'core-sitehome-enrolled-course-list',
templateUrl: 'enrolled-course-list.html',
})
export class CoreSiteHomeEnrolledCourseListComponent implements OnInit {
show: boolean;
constructor(private coursesProvider: CoreCoursesProvider) {}
/**
* Component being initialized.
*/
ngOnInit() {
if (this.coursesProvider.isMyCoursesDisabledInSite()) {
this.show = false;
} else {
return this.coursesProvider.getUserCourses().then((courses) => {
this.show = courses.length > 0;
});
}
}
}

View File

@ -0,0 +1,43 @@
<ion-content>
<ion-refresher [enabled]="dataLoaded" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="dataLoaded">
<ion-list>
<!-- Site home main contents. -->
<ng-container *ngIf="section && section.hasContent">
<ion-item text-wrap *ngIf="section.summary">
<core-format-text [text]="section.summary"></core-format-text>
</ion-item>
<core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId"></core-course-module>
</ng-container>
<!-- Site home items: news, categories, courses, etc. -->
<ng-container *ngIf="items.length > 0">
<ion-item-divider color="light" *ngIf="section && section.hasContent"></ion-item-divider>
<ng-container *ngFor="let item of items">
<core-sitehome-all-course-list class="item" *ngIf="item == 'all-course-list'"></core-sitehome-all-course-list>
<core-sitehome-categories *ngIf="item == 'categories'"></core-sitehome-categories>
<core-sitehome-course-search *ngIf="item == 'course-search'"></core-sitehome-course-search>
<core-sitehome-enrolled-course-list *ngIf="item == 'enrolled-course-list'"></core-sitehome-enrolled-course-list>
<core-sitehome-news *ngIf="item == 'news'"></core-sitehome-news>
</ng-container>
</ng-container>
<!-- Site home block. -->
<ng-container *ngIf="block && block.hasContent">
<ion-item-divider color="light" *ngIf="(section && section.hasContent) || items.length > 0"></ion-item-divider>
<ion-item text-wrap *ngIf="block.summary">
<core-format-text [text]="block.summary"></core-format-text>
</ion-item>
<core-course-module *ngFor="let module of block.modules" [module]="module" [courseId]="siteHomeId"></core-course-module>
</ng-container>
</ion-list>
<core-empty-box *ngIf="!hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box>
</core-loading>
</ion-content>

View File

@ -0,0 +1,143 @@
// (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, OnInit, Input } from '@angular/core';
import { IonicPage } from 'ionic-angular';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreCourseProvider } from '../../../course/providers/course';
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/module-prefetch-delegate';
/**
* Component that displays site home index.
*/
@Component({
selector: 'core-sitehome-index',
templateUrl: 'index.html',
})
export class CoreSiteHomeIndexComponent implements OnInit {
dataLoaded: boolean;
section: any;
block: any;
hasContent: boolean;
items: any[] = [];
siteHomeId: number;
protected sectionsLoaded: any[];
constructor(private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider,
private courseProvider: CoreCourseProvider, private courseHelper: CoreCourseHelperProvider,
private prefetchDelegate: CoreCourseModulePrefetchDelegate) {
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
}
/**
* Component being initialized.
*/
ngOnInit() {
this.loadContent().finally(() => {
this.dataLoaded = true;
});
}
/**
* Refresh the data.
*
* @param {any} refresher Refresher.
*/
doRefresh(refresher: any) {
const promises = [],
currentSite = this.sitesProvider.getCurrentSite();
promises.push(this.courseProvider.invalidateSections(this.siteHomeId));
promises.push(currentSite.invalidateConfig().then(() => {
// Config invalidated, fetch it again.
return currentSite.getConfig().then((config) => {
currentSite.setConfig(config);
});
}));
if (this.sectionsLoaded) {
// Invalidate modules prefetch data.
const modules = this.courseProvider.getSectionsModules(this.sectionsLoaded);
promises.push(this.prefetchDelegate.invalidateModules(modules, this.siteHomeId));
}
Promise.all(promises).finally(() => {
this.loadContent().finally(() => {
refresher.complete();
});
});
}
/**
* Convenience function to fetch the data.
*/
protected loadContent() {
this.hasContent = false;
let config = this.sitesProvider.getCurrentSite().getStoredConfig() || {numsections: 1};
if (config.frontpageloggedin) {
// Items with index 1 and 3 were removed on 2.5 and not being supported in the app.
let frontpageItems = [
'news', // News items.
false,
'categories', // List of categories.
false,
'categories', // Combo list.
'enrolled-course-list', // Enrolled courses.
'all-course-list', // List of courses.
'course-search' // Course search box.
],
items = config.frontpageloggedin.split(',');
this.items = [];
items.forEach((itemNumber) => {
// Get the frontpage item "name".
const item = frontpageItems[parseInt(itemNumber, 10)];
if (!item || this.items.indexOf(item) >= 0) {
return;
}
this.hasContent = true;
this.items.push(item);
});
}
return this.courseProvider.getSections(this.siteHomeId, false, true).then((sections) => {
this.sectionsLoaded = Array.from(sections);
// Check "Include a topic section" setting from numsections.
this.section = config.numsections && sections.length > 0 ? sections.pop() : false;
if (this.section) {
this.section.hasContent = this.courseHelper.sectionHasContent(this.section);
}
this.block = sections.length > 0 ? sections.pop() : false;
if (this.block) {
this.block.hasContent = this.courseHelper.sectionHasContent(this.block);
}
this.hasContent = this.courseHelper.addHandlerDataForModules(this.sectionsLoaded, this.siteHomeId) || this.hasContent;
// Add log in Moodle.
this.courseProvider.logView(this.siteHomeId);
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
});
}
}

View File

@ -0,0 +1 @@
<core-course-module class="core-sitehome-news" *ngIf="show" [module]="module" [courseId]="siteHomeId"></core-course-module>

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, OnInit, Input } from '@angular/core';
import { IonicPage } from 'ionic-angular';
import { CoreSitesProvider } from '../../../../providers/sites';
/**
* Component that displays site home news.
*/
@Component({
selector: 'core-sitehome-news',
templateUrl: 'news.html',
})
export class CoreSiteHomeNewsComponent implements OnInit {
module: any;
show: boolean;
siteHomeId: number;
constructor(private sitesProvider: CoreSitesProvider) {
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
}
/**
* Component being initialized.
*/
ngOnInit() {
// Get number of news items to show.
const newsItems = this.sitesProvider.getCurrentSite().getStoredConfig('newsitems') || 0;
if (!newsItems) {
return;
}
// @todo: Implement it once forum is supported.
// $mmaModForum = $mmAddonManager.get('$mmaModForum');
// if ($mmaModForum) {
// return $mmaModForum.getCourseForums(courseId).then(function(forums) {
// for (var x in forums) {
// if (forums[x].type == 'news') {
// return forums[x];
// }
// }
// }).then(function(forum) {
// if (forum) {
// return $mmCourse.getModuleBasicInfo(forum.cmid).then(function(module) {
// scope.show = true;
// scope.module = module;
// scope.module._controller =
// $mmCourseDelegate.getContentHandlerControllerFor(module.modname, module, courseId,
// module.section);
// });
// }
// });
// }
}
}

View File

@ -0,0 +1,4 @@
{
"sitehome": "Site home",
"sitenews": "Site announcements"
}

View File

@ -0,0 +1,6 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'core.sitehome.sitehome' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<core-sitehome-index></core-sitehome-index>

View File

@ -0,0 +1,31 @@
// (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 { CoreSiteHomeIndexPage } from './index';
import { CoreSiteHomeComponentsModule } from '../../components/components.module';
@NgModule({
declarations: [
CoreSiteHomeIndexPage,
],
imports: [
CoreSiteHomeComponentsModule,
IonicPageModule.forChild(CoreSiteHomeIndexPage),
TranslateModule.forChild()
]
})
export class CoreSiteHomeIndexPageModule {}

View File

@ -0,0 +1,37 @@
// (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, NavParams, NavController } from 'ionic-angular';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
/**
* Page that displays site home index.
*/
@IonicPage({segment: 'core-sitehome-index'})
@Component({
selector: 'page-core-sitehome-index',
templateUrl: 'index.html',
})
export class CoreSiteHomeIndexPage {
constructor(navParams: NavParams, navCtrl: NavController, courseHelper: CoreCourseHelperProvider,
sitesProvider: CoreSitesProvider) {
let module = navParams.get('module');
if (module) {
courseHelper.openModule(navCtrl, module, sitesProvider.getCurrentSite().getSiteHomeId());
}
}
}

View File

@ -0,0 +1,84 @@
// (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 { Injectable } from '@angular/core';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '../../contentlinks/providers/delegate';
import { CoreLoginHelperProvider } from '../../login/providers/helper';
import { CoreSiteHomeProvider } from './sitehome';
/**
* Handler to treat links to site home index.
*/
@Injectable()
export class CoreSiteHomeIndexLinkHandler extends CoreContentLinksHandlerBase {
name = 'CoreSiteHomeIndexLinkHandler';
featureName = '$mmSideMenuDelegate_mmaFrontpage';
pattern = /\/course\/view\.php.*([\?\&]id=\d+)/;
constructor(private sitesProvider: CoreSitesProvider, private siteHomeProvider: CoreSiteHomeProvider,
private loginHelper: CoreLoginHelperProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number) :
CoreContentLinksAction[]|Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?) => {
// Always use redirect to make it the new history root (to avoid "loops" in history).
this.loginHelper.redirect('CoreSiteHomeIndexPage', undefined, siteId);
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number) : boolean|Promise<boolean> {
courseId = parseInt(params.id, 10);
if (!courseId) {
return false;
}
return this.sitesProvider.getSite(siteId).then((site) => {
if (courseId != site.getSiteHomeId()) {
// The course is not site home.
return false;
}
return this.siteHomeProvider.isAvailable(siteId).then(() => {
return true;
}).catch(() => {
return false;
});
});
}
}

View File

@ -0,0 +1,62 @@
// (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 { Injectable } from '@angular/core';
import { CoreSiteHomeProvider } from './sitehome';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate';
import { CoreCoursesMyOverviewProvider } from '../../courses/providers/my-overview';
/**
* Handler to add Site Home into main menu.
*/
@Injectable()
export class CoreSiteHomeMainMenuHandler implements CoreMainMenuHandler {
name = 'CoreSiteHome';
priority = 1000;
isOverviewEnabled: boolean;
constructor(private siteHomeProvider: CoreSiteHomeProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) {}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean|Promise<boolean> {
// Check if my overview is enabled.
return this.myOverviewProvider.isEnabled().then((enabled) => {
if (enabled) {
// My overview is enabled, Site Home will be inside the overview page.
return false;
}
// My overview not enabled, check if site home is enabled.
return this.siteHomeProvider.isAvailable();
});
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreMainMenuHandlerData {
return {
icon: 'home',
title: 'core.sitehome.sitehome',
page: 'CoreSiteHomeIndexPage',
class: 'core-sitehome-handler'
};
}
}

View File

@ -0,0 +1,104 @@
// (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 { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreSite } from '../../../classes/site';
import { CoreCourseProvider } from '../../course/providers/course';
/**
* Service that provides some features regarding site home.
*/
@Injectable()
export class CoreSiteHomeProvider {
protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider) {
this.logger = logger.getInstance('CoreSiteHomeProvider');
}
/**
* Returns whether or not the frontpage is available for the current site.
*
* @param {string} [siteId] The site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: whether it's available.
*/
isAvailable(siteId?: string) : Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
// First check if it's disabled.
if (this.isDisabledInSite(site)) {
return false;
}
// Use a WS call to check if there's content in the site home.
const siteHomeId = site.getSiteHomeId(),
preSets = {emergencyCache: false};
this.logger.debug('Using WS call to check if site home is available.');
return this.courseProvider.getSections(siteHomeId, false, true, preSets, site.id).then((sections) : any => {
if (!sections || !sections.length) {
return Promise.reject(null);
}
for (let i = 0; i < sections.length; i++) {
let section = sections[i];
if (section.summary || (section.modules && section.modules.length)) {
// It has content, return true.
return true;
}
}
return Promise.reject(null);
}).catch(() => {
const config = site.getStoredConfig();
if (config && config.frontpageloggedin) {
const items = config.frontpageloggedin.split(',');
if (items.length > 0) {
// It's enabled.
return true;
}
}
return false;
});
}).catch(() => {
return false;
});
}
/**
* Check if Site Home is disabled in a certain site.
*
* @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise<boolean>} Promise resolved with true if disabled, rejected or resolved with false otherwise.
*/
isDisabled(siteId?: string) : Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return this.isDisabledInSite(site);
});
}
/**
* Check if Site Home is disabled in a certain site.
*
* @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether it's disabled.
*/
isDisabledInSite(site: CoreSite) : boolean {
site = site || this.sitesProvider.getCurrentSite();
return site.isFeatureDisabled('$mmSideMenuDelegate_mmaFrontpage');
}
}

View File

@ -0,0 +1,39 @@
// (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 { CoreSiteHomeProvider } from './providers/sitehome';
import { CoreSiteHomeMainMenuHandler } from './providers/mainmenu-handler';
import { CoreSiteHomeIndexLinkHandler } from './providers/index-link-handler';
import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate';
@NgModule({
declarations: [],
imports: [
],
providers: [
CoreSiteHomeProvider,
CoreSiteHomeMainMenuHandler,
CoreSiteHomeIndexLinkHandler
],
exports: []
})
export class CoreSiteHomeModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, contentLinksDelegate: CoreContentLinksDelegate,
mainMenuHandler: CoreSiteHomeMainMenuHandler, indexLinkHandler: CoreSiteHomeIndexLinkHandler) {
mainMenuDelegate.registerHandler(mainMenuHandler);
contentLinksDelegate.registerHandler(indexLinkHandler);
}
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
import { Platform } from 'ionic-angular';
import { Platform, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '../providers/app';
import { CoreFilepoolProvider } from '../providers/filepool';
@ -26,6 +26,7 @@ import { CoreUtilsProvider } from '../providers/utils/utils';
import { CoreSite } from '../classes/site';
import { CoreLinkDirective } from '../directives/link';
import { CoreExternalContentDirective } from '../directives/external-content';
import { CoreContentLinksHelperProvider } from '../core/contentlinks/providers/helper';
/**
* Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
@ -60,7 +61,8 @@ export class CoreFormatTextDirective implements OnChanges {
constructor(element: ElementRef, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider,
private textUtils: CoreTextUtilsProvider, private translate: TranslateService, private platform: Platform,
private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private loggerProvider: CoreLoggerProvider,
private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider) {
private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider,
private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController) {
this.element = element.nativeElement;
this.element.classList.add('opacity-hide'); // Hide contents until they're treated.
this.afterRender = new EventEmitter();
@ -274,7 +276,8 @@ export class CoreFormatTextDirective implements OnChanges {
// Important: We need to look for links first because in 'img' we add new links without core-link.
anchors.forEach((anchor) => {
// Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually.
let linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils);
let linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils,
this.contentLinksHelper, this.navCtrl);
linkDir.capture = true;
linkDir.ngOnInit();

View File

@ -13,10 +13,12 @@
// limitations under the License.
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreSitesProvider } from '../providers/sites';
import { CoreDomUtilsProvider } from '../providers/utils/dom';
import { CoreUrlUtilsProvider } from '../providers/utils/url';
import { CoreUtilsProvider } from '../providers/utils/utils';
import { CoreContentLinksHelperProvider } from '../core/contentlinks/providers/helper';
import { CoreConfigConstants } from '../configconstants';
/**
@ -36,7 +38,8 @@ export class CoreLinkDirective implements OnInit {
protected element: HTMLElement;
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider) {
private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider,
private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController) {
// This directive can be added dynamically. In that case, the first param is the anchor HTMLElement.
this.element = element.nativeElement || element;
}
@ -56,12 +59,11 @@ export class CoreLinkDirective implements OnInit {
event.stopPropagation();
if (this.utils.isTrueOrOne(this.capture)) {
// @todo: Handle link using content links helper.
// $mmContentLinksHelper.handleLink(href).then((treated) => {
// if (!treated) {
this.contentLinksHelper.handleLink(href, undefined, this.navCtrl).then((treated) => {
if (!treated) {
this.navigate(href);
// }
// });
}
});
} else {
this.navigate(href);
}