diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 7bd5875e5..6b6d5c152 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -31,7 +31,6 @@ import { CoreLoggerProvider } from '../providers/logger';
import { CoreDbProvider } from '../providers/db';
import { CoreAppProvider } from '../providers/app';
import { CoreConfigProvider } from '../providers/config';
-import { CoreEmulatorModule } from '../core/emulator/emulator.module';
import { CoreLangProvider } from '../providers/lang';
import { CoreTextUtilsProvider } from '../providers/utils/text';
import { CoreDomUtilsProvider } from '../providers/utils/dom';
@@ -53,10 +52,13 @@ import { CoreFilepoolProvider } from '../providers/filepool';
import { CoreUpdateManagerProvider } from '../providers/update-manager';
import { CorePluginFileDelegate } from '../providers/plugin-file-delegate';
+import { CoreComponentsModule } from '../components/components.module';
+import { CoreEmulatorModule } from '../core/emulator/emulator.module';
import { CoreLoginModule } from '../core/login/login.module';
import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module';
import { CoreCoursesModule } from '../core/courses/courses.module';
+
// For translate loader. AoT requires an exported function for factories.
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/lang/', '.json');
@@ -83,7 +85,8 @@ export function createTranslateLoader(http: HttpClient) {
CoreEmulatorModule,
CoreLoginModule,
CoreMainMenuModule,
- CoreCoursesModule
+ CoreCoursesModule,
+ CoreComponentsModule
],
bootstrap: [IonicApp],
entryComponents: [
diff --git a/src/components/components.module.ts b/src/components/components.module.ts
index 2b462dd4d..8be44288c 100644
--- a/src/components/components.module.ts
+++ b/src/components/components.module.ts
@@ -25,6 +25,9 @@ import { CoreProgressBarComponent } from './progress-bar/progress-bar';
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
import { CoreSearchBoxComponent } from './search-box/search-box';
import { CoreFileComponent } from './file/file';
+import { CoreContextMenuComponent } from './context-menu/context-menu';
+import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
+import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
@NgModule({
declarations: [
@@ -36,7 +39,13 @@ import { CoreFileComponent } from './file/file';
CoreProgressBarComponent,
CoreEmptyBoxComponent,
CoreSearchBoxComponent,
- CoreFileComponent
+ CoreFileComponent,
+ CoreContextMenuComponent,
+ CoreContextMenuItemComponent,
+ CoreContextMenuPopoverComponent
+ ],
+ entryComponents: [
+ CoreContextMenuPopoverComponent
],
imports: [
IonicModule,
@@ -52,7 +61,9 @@ import { CoreFileComponent } from './file/file';
CoreProgressBarComponent,
CoreEmptyBoxComponent,
CoreSearchBoxComponent,
- CoreFileComponent
+ CoreFileComponent,
+ CoreContextMenuComponent,
+ CoreContextMenuItemComponent
]
})
export class CoreComponentsModule {}
diff --git a/src/components/context-menu/context-menu-item.ts b/src/components/context-menu/context-menu-item.ts
new file mode 100644
index 000000000..61ab66d29
--- /dev/null
+++ b/src/components/context-menu/context-menu-item.ts
@@ -0,0 +1,114 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, Output, OnInit, OnDestroy, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
+import { CoreContextMenuComponent } from './context-menu';
+
+
+/**
+ * This directive adds a item to the Context Menu popover.
+ *
+ * @description
+ * This directive defines and item to be added to the popover generated in CoreContextMenu.
+ *
+ * It is required to place this tag inside a core-context-menu tag.
+ *
+ *
+ *
+ *
+ */
+@Component({
+ selector: 'core-context-menu-item',
+ template: ''
+})
+export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChanges {
+ @Input() content: string; // Content of the item.
+ @Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item.
+ @Input() iconAction?: string; // Name of the icon to be shown on the right side of the item. It represents the action to do on
+ // click. If is "spinner" an spinner will be shown. If no icon or spinner is selected, no action
+ // or link will work. If href but no iconAction is provided ion-arrow-right-c will be used.
+ @Input() ariaDescription?: string; // Aria label to add to iconDescription.
+ @Input() ariaAction?: string; // Aria label to add to iconAction. If not set, it will be equal to content.
+ @Input() href?: string; // Link to go if no action provided.
+ @Input() captureLink?: boolean|string; // Whether the link needs to be captured by the app.
+ @Input() autoLogin?: string; // Whether the link needs to be opened using auto-login.
+ @Input() closeOnClick?: boolean|string = true; // Whether to close the popover when the item is clicked.
+ @Input() priority?: number; // Used to sort items. The highest priority, the highest position.
+ @Input() badge?: string; // A badge to show in the item.
+ @Input() badgeClass?: number; // A class to set in the badge.
+ @Input() hidden?: boolean; // Whether the item should be hidden.
+ @Output() action?: EventEmitter; // Will emit an event when the item clicked.
+
+ protected hasAction = false;
+ protected destroyed = false;
+
+ constructor(private ctxtMenu: CoreContextMenuComponent) {
+ this.action = new EventEmitter();
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit() {
+ // Initialize values.
+ this.priority = this.priority || 1;
+ this.closeOnClick = this.getBooleanValue(this.closeOnClick, true);
+ this.hasAction = this.action.observers.length > 0;
+ this.ariaAction = this.ariaAction || this.content;
+
+ if (this.hasAction) {
+ this.href = '';
+ }
+
+ // Navigation help if href provided.
+ this.captureLink = this.href && this.captureLink ? this.captureLink : false;
+ this.autoLogin = this.autoLogin || 'check';
+
+ if (!this.destroyed) {
+ this.ctxtMenu.addItem(this);
+ }
+ }
+
+ /**
+ * Get a boolean value from item.
+ *
+ * @param {any} value Value to check.
+ * @param {boolean} defaultValue Value to use if undefined.
+ * @return {boolean} Boolean value.
+ */
+ protected getBooleanValue(value: any, defaultValue: boolean) : boolean {
+ if (typeof value == 'undefined') {
+ return defaultValue;
+ }
+ return value && value !== 'false';
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy() {
+ this.destroyed = true;
+ this.ctxtMenu.removeItem(this);
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}) {
+ if (changes.hidden && !changes.hidden.firstChange) {
+ this.ctxtMenu.itemsChanged();
+ }
+ }
+}
diff --git a/src/components/context-menu/context-menu-popover.html b/src/components/context-menu/context-menu-popover.html
new file mode 100644
index 000000000..145ee534c
--- /dev/null
+++ b/src/components/context-menu/context-menu-popover.html
@@ -0,0 +1,10 @@
+
+ {{title}}
+
+
+
+
+
+ {{item.badge}}
+
+
\ No newline at end of file
diff --git a/src/components/context-menu/context-menu-popover.ts b/src/components/context-menu/context-menu-popover.ts
new file mode 100644
index 000000000..fa144228e
--- /dev/null
+++ b/src/components/context-menu/context-menu-popover.ts
@@ -0,0 +1,69 @@
+// (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 { NavParams, ViewController } from 'ionic-angular';
+import { CoreContextMenuItemComponent } from './context-menu-item';
+
+/**
+ * Component to display a list of items received by param in a popover.
+ */
+@Component({
+ selector: 'core-context-menu-popover',
+ templateUrl: 'context-menu-popover.html'
+})
+export class CoreContextMenuPopoverComponent {
+ title: string;
+ items: CoreContextMenuItemComponent[];
+
+ constructor(navParams: NavParams, private viewCtrl: ViewController) {
+ this.title = navParams.get('title');
+ this.items = navParams.get('items') || [];
+ }
+
+ /**
+ * Close the popover.
+ */
+ closeMenu() : void {
+ this.viewCtrl.dismiss();
+ }
+
+ /**
+ * Function called when an item is clicked.
+ *
+ * @param {Event} event Click event.
+ * @param {CoreContextMenuItemComponent} item Item clicked.
+ * @return {boolean} Return true if success, false if error.
+ */
+ itemClicked(event: Event, item: CoreContextMenuItemComponent) : boolean {
+ if (item.action.observers.length > 0) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (!item.iconAction || item.iconAction == 'spinner') {
+ return false;
+ }
+
+ if (item.closeOnClick) {
+ this.closeMenu();
+ }
+
+ item.action.emit(this.closeMenu.bind(this));
+ } else if (item.href && item.closeOnClick) {
+ this.closeMenu();
+ }
+
+ return true;
+ }
+}
diff --git a/src/components/context-menu/context-menu.html b/src/components/context-menu/context-menu.html
new file mode 100644
index 000000000..94f724860
--- /dev/null
+++ b/src/components/context-menu/context-menu.html
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/src/components/context-menu/context-menu.ts b/src/components/context-menu/context-menu.ts
new file mode 100644
index 000000000..43ce298cc
--- /dev/null
+++ b/src/components/context-menu/context-menu.ts
@@ -0,0 +1,98 @@
+// (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 { PopoverController } from 'ionic-angular';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreContextMenuItemComponent } from './context-menu-item';
+import { CoreContextMenuPopoverComponent } from './context-menu-popover';
+import { Subject } from 'rxjs';
+
+/**
+ * This component adds a button (usually in the navigation bar) that displays a context menu popover.
+ */
+@Component({
+ selector: 'core-context-menu',
+ templateUrl: 'context-menu.html'
+})
+export class CoreContextMenuComponent implements OnInit {
+ @Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
+ @Input() title?: string; // Aria label and text to be shown on the top of the popover.
+
+ hideMenu: boolean;
+ ariaLabel: string;
+ protected items: CoreContextMenuItemComponent[] = [];
+ protected itemsChangedStream: Subject; // Stream to update the hideMenu boolean when items change.
+
+ constructor(private translate: TranslateService, private popoverCtrl: PopoverController) {
+ // Create the stream and subscribe to it. We ignore successive changes during 250ms.
+ this.itemsChangedStream = new Subject();
+ this.itemsChangedStream.auditTime(250).subscribe(() => {
+ // Hide the menu if all items are hidden.
+ this.hideMenu = !this.items.some((item) => {
+ return !item.hidden;
+ });
+ })
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit() {
+ this.icon = this.icon || 'more';
+ this.ariaLabel = this.title || this.translate.instant('core.info');
+ }
+
+ /**
+ * Add a context menu item.
+ *
+ * @param {CoreContextMenuItemComponent} item The item to add.
+ */
+ addItem(item: CoreContextMenuItemComponent) : void {
+ this.items.push(item);
+ this.itemsChanged();
+ }
+
+ /**
+ * Function called when the items change.
+ */
+ itemsChanged() {
+ this.itemsChangedStream.next();
+ }
+
+ /**
+ * Remove an item from the context menu.
+ *
+ * @param {CoreContextMenuItemComponent} item The item to remove.
+ */
+ removeItem(item: CoreContextMenuItemComponent) : void {
+ let index = this.items.indexOf(item);
+ if (index >= 0) {
+ this.items.splice(index, 1);
+ }
+ this.itemsChanged();
+ }
+
+ /**
+ * Show the context menu.
+ *
+ * @param {MouseEvent} event Event.
+ */
+ showContextMenu(event: MouseEvent) : void {
+ let popover = this.popoverCtrl.create(CoreContextMenuPopoverComponent, {title: this.title, items: this.items});
+ popover.present({
+ ev: event
+ });
+ }
+}
diff --git a/src/core/courses/pages/my-courses/my-courses.html b/src/core/courses/pages/my-courses/my-courses.html
index 720c27686..7a2eed8d4 100644
--- a/src/core/courses/pages/my-courses/my-courses.html
+++ b/src/core/courses/pages/my-courses/my-courses.html
@@ -6,7 +6,9 @@
-
+
+
+
diff --git a/src/core/courses/pages/my-overview/my-overview.html b/src/core/courses/pages/my-overview/my-overview.html
index 1d9e3a1aa..b45f3575f 100644
--- a/src/core/courses/pages/my-overview/my-overview.html
+++ b/src/core/courses/pages/my-overview/my-overview.html
@@ -49,7 +49,11 @@
-
+
+
+
+
+