MOBILE-2302 core: Implement context menu

main
Dani Palou 2017-12-22 08:06:01 +01:00
parent fc85a5b9c6
commit e6e17ae56d
10 changed files with 323 additions and 6 deletions

View File

@ -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: [

View File

@ -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 {}

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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
});
}
}

View File

@ -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>

View File

@ -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">

View File

@ -99,6 +99,8 @@
"lastdownloaded": "Last downloaded",
"lastmodified": "Last modified",
"lastsync": "Last synchronization",
"layoutgrid": "Grid",
"list": "List",
"listsep": ",",
"loading": "Loading",
"loadmore": "Load more",