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 { CoreDbProvider } from '../providers/db';
|
||||||
import { CoreAppProvider } from '../providers/app';
|
import { CoreAppProvider } from '../providers/app';
|
||||||
import { CoreConfigProvider } from '../providers/config';
|
import { CoreConfigProvider } from '../providers/config';
|
||||||
import { CoreEmulatorModule } from '../core/emulator/emulator.module';
|
|
||||||
import { CoreLangProvider } from '../providers/lang';
|
import { CoreLangProvider } from '../providers/lang';
|
||||||
import { CoreTextUtilsProvider } from '../providers/utils/text';
|
import { CoreTextUtilsProvider } from '../providers/utils/text';
|
||||||
import { CoreDomUtilsProvider } from '../providers/utils/dom';
|
import { CoreDomUtilsProvider } from '../providers/utils/dom';
|
||||||
|
@ -53,10 +52,13 @@ import { CoreFilepoolProvider } from '../providers/filepool';
|
||||||
import { CoreUpdateManagerProvider } from '../providers/update-manager';
|
import { CoreUpdateManagerProvider } from '../providers/update-manager';
|
||||||
import { CorePluginFileDelegate } from '../providers/plugin-file-delegate';
|
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 { CoreLoginModule } from '../core/login/login.module';
|
||||||
import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module';
|
import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module';
|
||||||
import { CoreCoursesModule } from '../core/courses/courses.module';
|
import { CoreCoursesModule } from '../core/courses/courses.module';
|
||||||
|
|
||||||
|
|
||||||
// For translate loader. AoT requires an exported function for factories.
|
// For translate loader. AoT requires an exported function for factories.
|
||||||
export function createTranslateLoader(http: HttpClient) {
|
export function createTranslateLoader(http: HttpClient) {
|
||||||
return new TranslateHttpLoader(http, './assets/lang/', '.json');
|
return new TranslateHttpLoader(http, './assets/lang/', '.json');
|
||||||
|
@ -83,7 +85,8 @@ export function createTranslateLoader(http: HttpClient) {
|
||||||
CoreEmulatorModule,
|
CoreEmulatorModule,
|
||||||
CoreLoginModule,
|
CoreLoginModule,
|
||||||
CoreMainMenuModule,
|
CoreMainMenuModule,
|
||||||
CoreCoursesModule
|
CoreCoursesModule,
|
||||||
|
CoreComponentsModule
|
||||||
],
|
],
|
||||||
bootstrap: [IonicApp],
|
bootstrap: [IonicApp],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
|
|
|
@ -25,6 +25,9 @@ import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||||
import { CoreSearchBoxComponent } from './search-box/search-box';
|
import { CoreSearchBoxComponent } from './search-box/search-box';
|
||||||
import { CoreFileComponent } from './file/file';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -36,7 +39,13 @@ import { CoreFileComponent } from './file/file';
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
CoreSearchBoxComponent,
|
CoreSearchBoxComponent,
|
||||||
CoreFileComponent
|
CoreFileComponent,
|
||||||
|
CoreContextMenuComponent,
|
||||||
|
CoreContextMenuItemComponent,
|
||||||
|
CoreContextMenuPopoverComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
CoreContextMenuPopoverComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
IonicModule,
|
IonicModule,
|
||||||
|
@ -52,7 +61,9 @@ import { CoreFileComponent } from './file/file';
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
CoreSearchBoxComponent,
|
CoreSearchBoxComponent,
|
||||||
CoreFileComponent
|
CoreFileComponent,
|
||||||
|
CoreContextMenuComponent,
|
||||||
|
CoreContextMenuItemComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
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">
|
<button *ngIf="searchEnabled" ion-button icon-only (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||||
<ion-icon name="search"></ion-icon>
|
<ion-icon name="search"></ion-icon>
|
||||||
</button>
|
</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-buttons>
|
||||||
</ion-navbar>
|
</ion-navbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
|
@ -49,7 +49,11 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
<ion-col col-1 class="text-right" [hidden]="!courses[courses.selected] || !courses[courses.selected].length">
|
<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-col>
|
||||||
</ion-row>
|
</ion-row>
|
||||||
<div [hidden]="!showFilter" class="mm-filter-box">
|
<div [hidden]="!showFilter" class="mm-filter-box">
|
||||||
|
|
|
@ -99,6 +99,8 @@
|
||||||
"lastdownloaded": "Last downloaded",
|
"lastdownloaded": "Last downloaded",
|
||||||
"lastmodified": "Last modified",
|
"lastmodified": "Last modified",
|
||||||
"lastsync": "Last synchronization",
|
"lastsync": "Last synchronization",
|
||||||
|
"layoutgrid": "Grid",
|
||||||
|
"list": "List",
|
||||||
"listsep": ",",
|
"listsep": ",",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"loadmore": "Load more",
|
"loadmore": "Load more",
|
||||||
|
|
Loading…
Reference in New Issue