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 @@ - + + + + +
diff --git a/src/lang/en.json b/src/lang/en.json index 64a2104f4..32ec7dfd3 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -99,6 +99,8 @@ "lastdownloaded": "Last downloaded", "lastmodified": "Last modified", "lastsync": "Last synchronization", + "layoutgrid": "Grid", + "list": "List", "listsep": ",", "loading": "Loading", "loadmore": "Load more",