MOBILE-2302 core: Implement context menu
parent
fc85a5b9c6
commit
e6e17ae56d
|
@ -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: [
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* <core-context-menu>
|
||||
* <core-context-menu-item [hidden]="showGrid" [priority]="601" [content]="'core.layoutgrid' | translate"
|
||||
* (action)="switchGrid()" [iconAction]="'apps'"></core-context-menu-item>
|
||||
* </core-context-menu>
|
||||
*/
|
||||
@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<string>; // 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<ion-list>
|
||||
<ion-list-header *ngIf="title">{{title}}</ion-list-header>
|
||||
<a ion-item text-wrap *ngFor="let item of items" core-link [capture]="item.captureLink" [autoLogin]="item.autoLogin" [href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden" [attr.detail-none]="!item.href || item.iconAction">
|
||||
<ion-icon *ngIf="item.iconDescription" [name]="item.iconDescription" [attr.aria-label]="item.ariaDescription" item-start></ion-icon>
|
||||
<core-format-text [clean]="true" [text]="item.content"></core-format-text>
|
||||
<ion-icon *ngIf="(item.href || item.action) && item.iconAction && item.iconAction != 'spinner'" [name]="item.iconAction" item-end></ion-icon>
|
||||
<ion-spinner *ngIf="(item.href || item.action) && item.iconAction == 'spinner'" item-end></ion-spinner>
|
||||
<ion-badge class="{{item.badgeClass}}" item-end *ngIf="item.badge">{{item.badge}}</ion-badge>
|
||||
</a>
|
||||
</ion-list>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<button [hidden]="hideMenu" ion-button clear icon-only [attr.aria-label]="ariaLabel" (click)="showContextMenu($event)">
|
||||
<ion-icon [name]="icon"></ion-icon>
|
||||
</button>
|
||||
<ng-content></ng-content>
|
|
@ -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<void>; // 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<void>();
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,7 +6,9 @@
|
|||
<button *ngIf="searchEnabled" ion-button icon-only (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||
<ion-icon name="search"></ion-icon>
|
||||
</button>
|
||||
<!-- @todo: Context menu. -->
|
||||
<core-context-menu>
|
||||
<core-context-menu-item [hidden]="!courses || courses.length <= 5" [priority]="700" [content]="'core.courses.filtermycourses' | translate" (action)="switchFilter()" [iconAction]="'funnel'"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -49,7 +49,11 @@
|
|||
</ion-item>
|
||||
</ion-col>
|
||||
<ion-col col-1 class="text-right" [hidden]="!courses[courses.selected] || !courses[courses.selected].length">
|
||||
<!-- @todo: Context menu. -->
|
||||
<core-context-menu>
|
||||
<core-context-menu-item [hidden]="!courses[courses.selected] || courses[courses.selected].length <= 5" [priority]="700" [content]="'core.courses.filtermycourses' | translate" (action)="switchFilter()" [iconAction]="'funnel'"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="showGrid" [priority]="601" [content]="'core.layoutgrid' | translate" (action)="switchGrid()" [iconAction]="'apps'"></core-context-menu-item>
|
||||
<core-context-menu-item [hidden]="!showGrid" [priority]="600" [content]="'core.list' | translate" (action)="switchGrid()" [iconAction]="'list'"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<div [hidden]="!showFilter" class="mm-filter-box">
|
||||
|
|
|
@ -99,6 +99,8 @@
|
|||
"lastdownloaded": "Last downloaded",
|
||||
"lastmodified": "Last modified",
|
||||
"lastsync": "Last synchronization",
|
||||
"layoutgrid": "Grid",
|
||||
"list": "List",
|
||||
"listsep": ",",
|
||||
"loading": "Loading",
|
||||
"loadmore": "Load more",
|
||||
|
|
Loading…
Reference in New Issue