MOBILE-2308 splitview: Implement Split view
parent
04e3d6d2d1
commit
27c715081c
|
@ -11,18 +11,17 @@
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
</ion-navbar>
|
</ion-navbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
<core-split-view>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-refresher [enabled]="eventsLoaded" (ionRefresh)="refreshEvents($event)">
|
<ion-refresher [enabled]="eventsLoaded" (ionRefresh)="refreshEvents($event)">
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
<core-loading [hideUntil]="eventsLoaded" class="core-loading-center">
|
<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 *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate">
|
||||||
</core-empty-box>
|
</core-empty-box>
|
||||||
|
|
||||||
<ion-list *ngIf="filteredEvents && filteredEvents.length">
|
<ion-list *ngIf="filteredEvents && filteredEvents.length">
|
||||||
<a ion-item text-wrap *ngFor="let event of filteredEvents" [title]="event.name" (click)="gotoEvent(event.id)">
|
<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">
|
||||||
<!-- core-split-view-link="site.calendar-event({id: event.id})" -->
|
|
||||||
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start>
|
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start>
|
||||||
<ion-icon *ngIf="!event.moduleIcon" name="{{event.icon}}" item-start></ion-icon>
|
<ion-icon *ngIf="!event.moduleIcon" name="{{event.icon}}" item-start></ion-icon>
|
||||||
<h2><core-format-text [text]="event.name"></core-format-text></h2>
|
<h2><core-format-text [text]="event.name"></core-format-text></h2>
|
||||||
|
@ -35,3 +34,4 @@
|
||||||
</ion-infinite-scroll>
|
</ion-infinite-scroll>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</ion-content>
|
</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 { CoreCoursePickerMenuPopoverComponent } from '../../../../components/course-picker-menu/course-picker-menu-popover';
|
||||||
import { CoreEventsProvider } from '../../../../providers/events';
|
import { CoreEventsProvider } from '../../../../providers/events';
|
||||||
import { CoreAppProvider } from '../../../../providers/app';
|
import { CoreAppProvider } from '../../../../providers/app';
|
||||||
|
import { CoreSplitViewComponent } from '../../../../components/split-view/split-view';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the list of calendar events.
|
* Page that displays the list of calendar events.
|
||||||
|
@ -36,6 +37,7 @@ import { CoreAppProvider } from '../../../../providers/app';
|
||||||
})
|
})
|
||||||
export class AddonCalendarListPage implements OnDestroy {
|
export class AddonCalendarListPage implements OnDestroy {
|
||||||
@ViewChild(Content) content: Content;
|
@ViewChild(Content) content: Content;
|
||||||
|
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||||
|
|
||||||
protected daysLoaded = 0;
|
protected daysLoaded = 0;
|
||||||
protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events.
|
protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events.
|
||||||
|
@ -56,7 +58,6 @@ export class AddonCalendarListPage implements OnDestroy {
|
||||||
events = [];
|
events = [];
|
||||||
notificationsEnabled = false;
|
notificationsEnabled = false;
|
||||||
filteredEvents = [];
|
filteredEvents = [];
|
||||||
eventToLoad = 1;
|
|
||||||
canLoadMore = false;
|
canLoadMore = false;
|
||||||
filter = {
|
filter = {
|
||||||
course: this.allCourses
|
course: this.allCourses
|
||||||
|
@ -84,26 +85,15 @@ export class AddonCalendarListPage implements OnDestroy {
|
||||||
* View loaded.
|
* View loaded.
|
||||||
*/
|
*/
|
||||||
ionViewDidLoad() {
|
ionViewDidLoad() {
|
||||||
if (this.eventId && !this.appProvider.isWide()) {
|
if (this.eventId) {
|
||||||
// There is an event to load and it's a phone device, open the event in a new state.
|
// There is an event to load, open the event in a new state.
|
||||||
this.gotoEvent(this.eventId);
|
this.gotoEvent(this.eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchData().then(() => {
|
this.fetchData().then(() => {
|
||||||
// @TODO: Split view once single event is done.
|
if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) {
|
||||||
if (this.eventId && this.appProvider.isWide()) {
|
// Take first and load it.
|
||||||
// There is an event to load and it's a tablet device. Search the position of the event in the list and load it.
|
this.gotoEvent(this.events[0].id);
|
||||||
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);
|
|
||||||
//});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.eventsLoaded = true;
|
this.eventsLoaded = true;
|
||||||
|
@ -324,7 +314,8 @@ export class AddonCalendarListPage implements OnDestroy {
|
||||||
* Navigate to a particular event.
|
* Navigate to a particular event.
|
||||||
*/
|
*/
|
||||||
gotoEvent(eventId) {
|
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 { CoreMarkRequiredComponent } from './mark-required/mark-required';
|
||||||
import { CoreInputErrorsComponent } from './input-errors/input-errors';
|
import { CoreInputErrorsComponent } from './input-errors/input-errors';
|
||||||
import { CoreShowPasswordComponent } from './show-password/show-password';
|
import { CoreShowPasswordComponent } from './show-password/show-password';
|
||||||
|
import { CoreSplitViewComponent } from './split-view/split-view';
|
||||||
import { CoreIframeComponent } from './iframe/iframe';
|
import { CoreIframeComponent } from './iframe/iframe';
|
||||||
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||||
|
@ -40,6 +41,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
CoreMarkRequiredComponent,
|
CoreMarkRequiredComponent,
|
||||||
CoreInputErrorsComponent,
|
CoreInputErrorsComponent,
|
||||||
CoreShowPasswordComponent,
|
CoreShowPasswordComponent,
|
||||||
|
CoreSplitViewComponent,
|
||||||
CoreIframeComponent,
|
CoreIframeComponent,
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
|
@ -68,6 +70,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
CoreMarkRequiredComponent,
|
CoreMarkRequiredComponent,
|
||||||
CoreInputErrorsComponent,
|
CoreInputErrorsComponent,
|
||||||
CoreShowPasswordComponent,
|
CoreShowPasswordComponent,
|
||||||
|
CoreSplitViewComponent,
|
||||||
CoreIframeComponent,
|
CoreIframeComponent,
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreEmptyBoxComponent,
|
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;
|
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.
|
* Check if the app is running in a Windows environment.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue