MOBILE-2308 splitview: Implement Split view
parent
04e3d6d2d1
commit
27c715081c
|
@ -11,27 +11,27 @@
|
|||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="eventsLoaded" (ionRefresh)="refreshEvents($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="eventsLoaded" class="core-loading-center">
|
||||
<!-- @todo: Split view. -->
|
||||
<core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate">
|
||||
</core-empty-box>
|
||||
<core-split-view>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="eventsLoaded" (ionRefresh)="refreshEvents($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="eventsLoaded" class="core-loading-center">
|
||||
<core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate">
|
||||
</core-empty-box>
|
||||
|
||||
<ion-list *ngIf="filteredEvents && filteredEvents.length">
|
||||
<a ion-item text-wrap *ngFor="let event of filteredEvents" [title]="event.name" (click)="gotoEvent(event.id)">
|
||||
<!-- core-split-view-link="site.calendar-event({id: event.id})" -->
|
||||
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start>
|
||||
<ion-icon *ngIf="!event.moduleIcon" name="{{event.icon}}" item-start></ion-icon>
|
||||
<h2><core-format-text [text]="event.name"></core-format-text></h2>
|
||||
<p>{{ event.timestart | coreToLocaleString }}</p>
|
||||
</a>
|
||||
</ion-list>
|
||||
<ion-list *ngIf="filteredEvents && filteredEvents.length">
|
||||
<a ion-item text-wrap *ngFor="let event of filteredEvents" [title]="event.name" (click)="gotoEvent(event.id)" [class.core-split-item-selected]="event.id == eventId">
|
||||
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start>
|
||||
<ion-icon *ngIf="!event.moduleIcon" name="{{event.icon}}" item-start></ion-icon>
|
||||
<h2><core-format-text [text]="event.name"></core-format-text></h2>
|
||||
<p>{{ event.timestart | coreToLocaleString }}</p>
|
||||
</a>
|
||||
</ion-list>
|
||||
|
||||
<ion-infinite-scroll [enabled]="canLoadMore" (ionInfinite)="$event.waitFor(fetchEvents())">
|
||||
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
<ion-infinite-scroll [enabled]="canLoadMore" (ionInfinite)="$event.waitFor(fetchEvents())">
|
||||
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
</core-split-view>
|
|
@ -25,6 +25,7 @@ import { CoreLocalNotificationsProvider } from '../../../../providers/local-noti
|
|||
import { CoreCoursePickerMenuPopoverComponent } from '../../../../components/course-picker-menu/course-picker-menu-popover';
|
||||
import { CoreEventsProvider } from '../../../../providers/events';
|
||||
import { CoreAppProvider } from '../../../../providers/app';
|
||||
import { CoreSplitViewComponent } from '../../../../components/split-view/split-view';
|
||||
|
||||
/**
|
||||
* Page that displays the list of calendar events.
|
||||
|
@ -36,6 +37,7 @@ import { CoreAppProvider } from '../../../../providers/app';
|
|||
})
|
||||
export class AddonCalendarListPage implements OnDestroy {
|
||||
@ViewChild(Content) content: Content;
|
||||
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||
|
||||
protected daysLoaded = 0;
|
||||
protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events.
|
||||
|
@ -56,7 +58,6 @@ export class AddonCalendarListPage implements OnDestroy {
|
|||
events = [];
|
||||
notificationsEnabled = false;
|
||||
filteredEvents = [];
|
||||
eventToLoad = 1;
|
||||
canLoadMore = false;
|
||||
filter = {
|
||||
course: this.allCourses
|
||||
|
@ -84,26 +85,15 @@ export class AddonCalendarListPage implements OnDestroy {
|
|||
* View loaded.
|
||||
*/
|
||||
ionViewDidLoad() {
|
||||
if (this.eventId && !this.appProvider.isWide()) {
|
||||
// There is an event to load and it's a phone device, open the event in a new state.
|
||||
if (this.eventId) {
|
||||
// There is an event to load, open the event in a new state.
|
||||
this.gotoEvent(this.eventId);
|
||||
}
|
||||
|
||||
this.fetchData().then(() => {
|
||||
// @TODO: Split view once single event is done.
|
||||
if (this.eventId && this.appProvider.isWide()) {
|
||||
// There is an event to load and it's a tablet device. Search the position of the event in the list and load it.
|
||||
let found = this.events.findIndex((e) => {return e.id == this.eventId});
|
||||
|
||||
if (found > 0) {
|
||||
this.eventToLoad = found + 1;
|
||||
} else {
|
||||
// Event not found in the list, open it in a new state. Use a $timeout to open the state after the
|
||||
// split view is loaded.
|
||||
//$timeout(function() {
|
||||
this.gotoEvent(this.eventId);
|
||||
//});
|
||||
}
|
||||
if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) {
|
||||
// Take first and load it.
|
||||
this.gotoEvent(this.events[0].id);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.eventsLoaded = true;
|
||||
|
@ -324,7 +314,8 @@ export class AddonCalendarListPage implements OnDestroy {
|
|||
* Navigate to a particular event.
|
||||
*/
|
||||
gotoEvent(eventId) {
|
||||
this.navCtrl.push('AddonCalendarEventPage', {id: eventId});
|
||||
this.eventId = eventId;
|
||||
this.splitviewCtrl.push('AddonCalendarEventPage', {id: eventId});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,6 +21,7 @@ import { CoreLoadingComponent } from './loading/loading';
|
|||
import { CoreMarkRequiredComponent } from './mark-required/mark-required';
|
||||
import { CoreInputErrorsComponent } from './input-errors/input-errors';
|
||||
import { CoreShowPasswordComponent } from './show-password/show-password';
|
||||
import { CoreSplitViewComponent } from './split-view/split-view';
|
||||
import { CoreIframeComponent } from './iframe/iframe';
|
||||
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||
|
@ -40,6 +41,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
|
|||
CoreMarkRequiredComponent,
|
||||
CoreInputErrorsComponent,
|
||||
CoreShowPasswordComponent,
|
||||
CoreSplitViewComponent,
|
||||
CoreIframeComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
|
@ -68,6 +70,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
|
|||
CoreMarkRequiredComponent,
|
||||
CoreInputErrorsComponent,
|
||||
CoreShowPasswordComponent,
|
||||
CoreSplitViewComponent,
|
||||
CoreIframeComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title> </ion-title>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<core-empty-box icon="arrow-dropleft" [message]="'core.emptysplit' | translate"></core-empty-box>
|
||||
</ion-content>
|
|
@ -0,0 +1,20 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { CoreSplitViewPlaceholderPage } from './placeholder';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '../../components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreSplitViewPlaceholderPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
IonicPageModule.forChild(CoreSplitViewPlaceholderPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
exports: [
|
||||
CoreSplitViewPlaceholderPage
|
||||
]
|
||||
})
|
||||
export class CorePlaceholderPageModule { }
|
|
@ -0,0 +1,3 @@
|
|||
core-placeholder {
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { Component } from '@angular/core';
|
||||
import {
|
||||
IonicPage,
|
||||
NavController,
|
||||
NavParams } from 'ionic-angular';
|
||||
|
||||
@IonicPage()
|
||||
@Component({
|
||||
selector: 'core-placeholder',
|
||||
templateUrl: 'placeholder.html',
|
||||
})
|
||||
export class CoreSplitViewPlaceholderPage {
|
||||
|
||||
constructor(public navCtrl: NavController, public navParams: NavParams) { }
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<ion-split-pane (ionChange)="onSplitPaneChanged($event._visible);" [when]="when">
|
||||
<ion-menu [content]="detailNav" type="push">
|
||||
<ion-header><ion-toolbar><ion-title></ion-title></ion-toolbar></ion-header>
|
||||
<ng-content></ng-content>
|
||||
</ion-menu>
|
||||
<ion-nav [root]="detailPage" #detailNav main></ion-nav>
|
||||
</ion-split-pane>
|
|
@ -0,0 +1,39 @@
|
|||
core-split-view {
|
||||
ion-menu.split-pane-side {
|
||||
display: block;
|
||||
|
||||
.menu-inner {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
-webkit-transform: initial;
|
||||
transform: initial;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.split-pane-main {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.split-pane-visible {
|
||||
.split-pane-main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.split-pane-side .core-split-item-selected {
|
||||
background-color: $gray-lighter;
|
||||
border-left: 5px solid $core-color-light;
|
||||
&.item-md {
|
||||
padding-left: $item-md-padding-start - 5px;
|
||||
}
|
||||
&.item-ios {
|
||||
padding-left: $item-ios-padding-start - 5px;
|
||||
}
|
||||
&.item-wp {
|
||||
padding-left: $item-wp-padding-start - 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
// (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.
|
||||
|
||||
// Code based on https://github.com/martinpritchardelevate/ionic-split-pane-demo
|
||||
|
||||
import { Component, ViewChild, Injectable, Input, ElementRef, OnInit } from '@angular/core';
|
||||
import { NavController, Nav } from 'ionic-angular';
|
||||
import { CoreSplitViewPlaceholderPage } from './placeholder/placeholder';
|
||||
|
||||
/**
|
||||
* Directive to create a split view layout.
|
||||
*
|
||||
* @description
|
||||
* To init/change the right pane contents (content pane), inject this component in the master page.
|
||||
* @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||
* Then use the push function to load.
|
||||
*
|
||||
* Accepts the following params:
|
||||
*
|
||||
* @param {string|boolean} [when] When the split-pane should be shown. Can be a CSS media query expression, or a shortcut
|
||||
* expression. Can also be a boolean expression. Check split-pane component documentation for more information.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <core-split-view [when]="lg">
|
||||
* <ion-content><!-- CONTENT TO SHOW ON THE LEFT PANEL (MENU) --></ion-content>
|
||||
* </core-split-view>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-split-view',
|
||||
templateUrl: 'split-view.html'
|
||||
})
|
||||
export class CoreSplitViewComponent implements OnInit {
|
||||
// @todo Mix both panels header buttons
|
||||
|
||||
@ViewChild('detailNav') _detailNav: Nav;
|
||||
@Input() when?: string | boolean = "md"; //
|
||||
protected _isOn: boolean = false;
|
||||
protected masterPageName: string = "";
|
||||
protected loadDetailPage: any = false;
|
||||
protected element: HTMLElement; // Current element.
|
||||
|
||||
// Empty placeholder for the 'detail' page.
|
||||
detailPage: any = null;
|
||||
|
||||
constructor(private _masterNav: NavController, element: ElementRef) {
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit() {
|
||||
// Get the master page name and set an empty page as a placeholder.
|
||||
this.masterPageName = this._masterNav.getActive().component.name;
|
||||
this.emptyDetails();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if both panels are shown. It depends on screen width.
|
||||
*
|
||||
* @return {boolean} If split view is enabled.
|
||||
*/
|
||||
isOn(): boolean {
|
||||
return this._isOn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a page to the navigation stack. It will decide where to load it depending on the size of the screen.
|
||||
*
|
||||
* @param {any} page The component class or deeplink name you want to push onto the navigation stack.
|
||||
* @param {any} params Any NavParams you want to pass along to the next view.
|
||||
*/
|
||||
push(page: any, params?: any, element?: HTMLElement) {
|
||||
if (this._isOn) {
|
||||
this._detailNav.setRoot(page, params);
|
||||
} else {
|
||||
this.loadDetailPage = {
|
||||
component: page,
|
||||
data: params
|
||||
};
|
||||
this._masterNav.push(page, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the details panel to default info.
|
||||
*/
|
||||
emptyDetails() {
|
||||
this.loadDetailPage = false;
|
||||
this._detailNav.setRoot('CoreSplitViewPlaceholderPage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Splitpanel visibility has changed.
|
||||
*
|
||||
* @param {Boolean} isOn If it fits both panels at the same time.
|
||||
*/
|
||||
onSplitPaneChanged(isOn) {
|
||||
this._isOn = isOn;
|
||||
if (this._masterNav && this._detailNav) {
|
||||
(isOn) ? this.activateSplitView() : this.deactivateSplitView();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the split view, show both panels and do some magical navigation.
|
||||
*/
|
||||
activateSplitView() {
|
||||
let currentView = this._masterNav.getActive(),
|
||||
currentPageName = currentView.component.name;
|
||||
if (currentPageName != this.masterPageName) {
|
||||
// CurrentView is a 'Detail' page remove it from the 'master' nav stack.
|
||||
this._masterNav.pop();
|
||||
|
||||
// and add it to the 'detail' nav stack.
|
||||
this._detailNav.setRoot(currentView.component, currentView.data);
|
||||
} else if (this.loadDetailPage) {
|
||||
// MasterPage is shown, load the last detail page if found.
|
||||
this._detailNav.setRoot(this.loadDetailPage.component, this.loadDetailPage.data);
|
||||
}
|
||||
this.loadDetailPage = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disabled the split view, show only one panel and do some magical navigation.
|
||||
*/
|
||||
deactivateSplitView() {
|
||||
let detailView = this._detailNav.getActive(),
|
||||
currentPageName = detailView.component.name;
|
||||
if (currentPageName != 'CoreSplitViewPlaceholderPage') {
|
||||
// Current detail view is a 'Detail' page so, not the placeholder page, push it on 'master' nav stack.
|
||||
this._masterNav.push(detailView.component, detailView.data);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -200,16 +200,6 @@ export class CoreAppProvider {
|
|||
return limited.indexOf(type) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is wide enough. It's used i.e. to show split view.
|
||||
*
|
||||
* @return {boolean} Whether the device uses a limited connection.
|
||||
*/
|
||||
isWide() : boolean {
|
||||
//@todo Should use media querys like splitpane
|
||||
return this.platform.is('tablet');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app is running in a Windows environment.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue