Merge pull request #1338 from crazyserver/MOBILE-2430

Mobile 2430
main
Juan Leyva 2018-06-15 10:23:48 +02:00 committed by GitHub
commit 4a70a972e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 665 additions and 79 deletions

View File

@ -3,7 +3,7 @@
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-content [class.has-fab]="showUpload && root != 'site' && !path">
<ion-refresher [enabled]="filesLoaded && (showPrivateFiles || showSiteFiles)" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -95,5 +95,10 @@
<a (click)="searchReset()">{{ 'addon.mod_data.resetsettings' | translate}}</a>
</core-empty-box>
</core-loading>
<ion-fab bottom right *ngIf="canAdd">
<button ion-fab (click)="gotoAddEntries($event)" [attr.aria-label]="'addon.mod_data.addentries' | translate">
<ion-icon name="add"></ion-icon>
</button>
</ion-fab>

View File

@ -12,7 +12,7 @@
<!-- Content. -->
<core-split-view>
<ion-content>
<ion-content [class.has-fab]="forum && forum.cancreatediscussions">
<ion-refresher [enabled]="loaded" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
@ -25,11 +25,6 @@
</ion-card>
<ng-container *ngIf="forum && discussions.length > 0">
<div padding-horizontal margin-vertical *ngIf="forum.cancreatediscussions">
<button ion-button block (click)="openNewDiscussion()">
{{addDiscussionText}}
</button>
</div>
<ion-card *ngFor="let discussion of offlineDiscussions" (click)="openNewDiscussion(discussion.timecreated)" [class.addon-forum-discussion-selected]="discussion.timecreated == -selectedDiscussion">
<ion-item text-wrap>
<ion-avatar item-start core-user-link [userId]="discussion.userid" [courseId]="courseId">
@ -97,5 +92,11 @@
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</core-loading>
<ion-fab bottom right *ngIf="forum && forum.cancreatediscussions">
<button ion-fab (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText">
<ion-icon name="add"></ion-icon>
</button>
</ion-fab>
</ion-content>
</core-split-view>

View File

@ -25,7 +25,7 @@
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-tabs [selectedIndex]="selectedTab">
<core-tabs [hideUntil]="loaded" [selectedIndex]="selectedTab">
<!-- Page contents. -->
<core-tab [title]="'addon.mod_wiki.viewpage' | translate" icon="document">
<ng-template>

View File

@ -15,7 +15,7 @@
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule, COMPILER_OPTIONS } from '@angular/core';
import { IonicApp, IonicModule, Platform, Content, ScrollEvent } from 'ionic-angular';
import { IonicApp, IonicModule, Platform, Content, ScrollEvent, Config } from 'ionic-angular';
import { assert } from 'ionic-angular/util/util';
import { HttpModule } from '@angular/http';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
@ -26,6 +26,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { MoodleMobileApp } from './app.component';
import { CoreInterceptor } from '@classes/interceptor';
import { CorePageTransition } from '@classes/page-transition';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreDbProvider } from '@providers/db';
import { CoreAppProvider } from '@providers/app';
@ -154,7 +155,7 @@ export const CORE_PROVIDERS: any[] = [
HttpClientModule, // HttpClient is used to make JSON requests. It fails for HEAD requests because there is no content.
HttpModule,
IonicModule.forRoot(MoodleMobileApp, {
pageTransition: 'ios-transition'
pageTransition: 'core-page-transition'
}),
TranslateModule.forRoot({
loader: {
@ -257,7 +258,7 @@ export const CORE_PROVIDERS: any[] = [
]
})
export class AppModule {
constructor(platform: Platform, initDelegate: CoreInitDelegate, updateManager: CoreUpdateManagerProvider,
constructor(platform: Platform, initDelegate: CoreInitDelegate, updateManager: CoreUpdateManagerProvider, config: Config,
sitesProvider: CoreSitesProvider, fileProvider: CoreFileProvider) {
// Register a handler for platform ready.
initDelegate.registerProcess({
@ -289,6 +290,9 @@ export class AppModule {
// Execute the init processes.
initDelegate.executeInitProcesses();
// Set transition animation.
config.setTransition('core-page-transition', CorePageTransition);
// Decorate ion-content.
this.decorateIonContent();
}

View File

@ -370,7 +370,13 @@ ion-col ion-select {
white-space: normal;
text-align: right;
}
}
.core-button-select {
ion-icon:last-child {
margin-left: 5px;
}
}
// File uploader.
// -------------------------
@ -677,6 +683,16 @@ canvas[core-chart] {
}
}
.has-fab .scroll-content{
padding-bottom: 56px;
}
.scroll-content ion-fab {
position: fixed;
margin-bottom: 56px;
}
// For some reason, in iOS the pages don't inherit the background-color from ion-app, set it explicitly.
.ion-page {
background-color: $background-color;

View File

@ -0,0 +1,143 @@
// (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 { Animation } from 'ionic-angular/animations/animation';
import { isPresent } from 'ionic-angular/util/util';
import { PageTransition } from 'ionic-angular/transitions/page-transition';
const DURATION = 500;
const EASING = 'cubic-bezier(0.36,0.66,0.04,1)';
const OPACITY = 'opacity';
const TRANSFORM = 'transform';
const TRANSLATEX = 'translateX';
const CENTER = '0%';
const OFF_OPACITY = 0.8;
const SHOW_BACK_BTN_CSS = 'show-back-button';
/**
* This class overrides the default transition to avoid glitches with new tabs and split view.
* Is based on IOSTransition class but it has some changes:
* - The animation is done to the full page not header, footer and content separetely.
* - On the Navbar only the back button is animated (title and other buttons will be done as a whole). Otherwise back button won't
* appear.
*/
export class CorePageTransition extends PageTransition {
init(): void {
super.init();
const plt = this.plt;
const OFF_RIGHT = plt.isRTL ? '-99.5%' : '99.5%';
const OFF_LEFT = plt.isRTL ? '33%' : '-33%';
const enteringView = this.enteringView;
const leavingView = this.leavingView;
const opts = this.opts;
this.duration(isPresent(opts.duration) ? opts.duration : DURATION);
this.easing(isPresent(opts.easing) ? opts.easing : EASING);
const backDirection = (opts.direction === 'back');
const enteringHasNavbar = (enteringView && enteringView.hasNavbar());
const leavingHasNavbar = (leavingView && leavingView.hasNavbar());
if (enteringView) {
// Get the native element for the entering page.
const enteringPageEle = enteringView.pageRef().nativeElement;
// Entering content.
const enteringContent = new Animation(plt, enteringPageEle);
this.add(enteringContent);
if (backDirection) {
// Entering content, back direction.
enteringContent
.fromTo(TRANSLATEX, OFF_LEFT, CENTER, true)
.fromTo(OPACITY, OFF_OPACITY, 1, true);
}
else {
// Entering content, forward direction.
enteringContent
.beforeClearStyles([OPACITY])
.fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true);
}
if (enteringHasNavbar) {
// Entering page has a navbar.
const enteringNavbarEle = enteringPageEle.querySelector('ion-navbar');
const enteringNavBar = new Animation(plt, enteringNavbarEle);
this.add(enteringNavBar);
const enteringBackButton = new Animation(plt, enteringNavbarEle.querySelector('.back-button'));
enteringNavBar
.add(enteringBackButton);
// Set properties depending on direction.
if (backDirection) {
// Entering navbar, back direction.
if (enteringView.enableBack()) {
// Back direction, entering page has a back button.
enteringBackButton
.beforeAddClass(SHOW_BACK_BTN_CSS)
.fromTo(OPACITY, 0.01, 1, true);
}
}
else {
// Entering navbar, forward direction.
if (enteringView.enableBack()) {
// Forward direction, entering page has a back button.
enteringBackButton
.beforeAddClass(SHOW_BACK_BTN_CSS)
.fromTo(OPACITY, 0.01, 1, true);
const enteringBackBtnText = new Animation(plt, enteringNavbarEle.querySelector('.back-button-text'));
enteringBackBtnText.fromTo(TRANSLATEX, (plt.isRTL ? '-100px' : '100px'), '0px');
enteringNavBar.add(enteringBackBtnText);
}
else {
enteringBackButton.beforeRemoveClass(SHOW_BACK_BTN_CSS);
}
}
}
}
// Setup leaving view.
if (leavingView && leavingView.pageRef()) {
// Leaving content.
const leavingPageEle = leavingView.pageRef().nativeElement;
const leavingContent = new Animation(plt, leavingPageEle);
this.add(leavingContent);
if (backDirection) {
// Leaving content, back direction.
leavingContent
.beforeClearStyles([OPACITY])
.fromTo(TRANSLATEX, CENTER, (plt.isRTL ? '-100%' : '100%'));
}
else {
// Leaving content, forward direction.
leavingContent
.fromTo(TRANSLATEX, CENTER, OFF_LEFT)
.fromTo(OPACITY, 1, OFF_OPACITY)
.afterClearStyles([TRANSFORM, OPACITY]);
}
if (leavingHasNavbar) {
// Leaving page has a navbar.
const leavingNavbarEle = leavingPageEle.querySelector('ion-navbar');
const leavingNavBar = new Animation(plt, leavingNavbarEle);
const leavingBackButton = new Animation(plt, leavingNavbarEle.querySelector('.back-button'));
leavingNavBar
.add(leavingBackButton);
this.add(leavingNavBar);
// Fade out leaving navbar items.
leavingBackButton.fromTo(OPACITY, 0.99, 0);
if (backDirection) {
const leavingBackBtnText = new Animation(plt, leavingNavbarEle.querySelector('.back-button-text'));
leavingBackBtnText.fromTo(TRANSLATEX, CENTER, (plt.isRTL ? -300 : 300) + 'px');
leavingNavBar.add(leavingBackBtnText);
}
else {
// Leaving navbar, forward direction.
leavingBackButton.afterClearStyles([OPACITY]);
}
}
}
}
}

View File

@ -19,6 +19,7 @@ core-empty-box {
display: table-cell;
text-align: center;
vertical-align: middle;
pointer-events: auto;
}
&.core-empty-box-inline {

View File

@ -5,18 +5,18 @@
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
<div #decorate class="core-rte-toolbar">
<div class="core-rte-buttons">
<button data-command="bold"><strong>B</strong></button>
<button data-command="italic"><i>I</i></button>
<button data-command="underline"><u>U</u></button>
<button data-command="formatBlock|<p>">Normal</button>
<button data-command="formatBlock|<h1>">H1</button>
<button data-command="formatBlock|<h2>">H2</button>
<button data-command="formatBlock|<h3>">H3</button>
<button data-command="formatBlock|<pre>">&lt;pre&gt;</button>
<button data-command="insertOrderedList"><ion-icon name="list" md="ios-list"></ion-icon></button>
<button data-command="insertUnorderedList">1,2,3</button>
<button data-command="removeFormat"><ion-icon name="brush"></ion-icon></button>
<button (click)="toggleEditor($event)"><ion-icon name="eye-off"></ion-icon> {{ 'core.viewcode' | translate }}</button>
<button data-command="bold"><core-icon name="fa-bold"></core-icon></button>
<button data-command="italic"><core-icon name="fa-italic"></core-icon></button>
<button data-command="underline"><core-icon name="fa-underline"></core-icon></button>
<button data-command="strikeThrough"><core-icon name="fa-strikethrough"></core-icon></button>
<button data-command="formatBlock|<p>"><core-icon name="fa-paragraph"></core-icon></button>
<button data-command="formatBlock|<h1>"><core-icon name="fa-header"></core-icon>1</button>
<button data-command="formatBlock|<h2>"><core-icon name="fa-header"></core-icon>2</button>
<button data-command="formatBlock|<h3>"><core-icon name="fa-header"></core-icon>3</button>
<button data-command="insertUnorderedList"><core-icon name="fa-list-ul"></core-icon></button>
<button data-command="insertOrderedList"><core-icon name="fa-list-ol"></core-icon></button>
<button data-command="removeFormat"><core-icon name="fa-eraser"></core-icon></button>
<button (click)="toggleEditor($event)"><core-icon name="fa-code"></core-icon> {{ 'core.viewcode' | translate }}</button>
</div>
</div>
</div>
@ -25,7 +25,7 @@
<ion-textarea #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name" ngControl="control" (ionChange)="onChange($event)"></ion-textarea>
<div class="core-rte-toolbar">
<div #decorate class="core-rte-buttons">
<button tappable (click)="toggleEditor($event)"><ion-icon name="eye"></ion-icon> {{ 'core.vieweditor' | translate }}</button>
<button tappable (click)="toggleEditor($event)"><core-icon name="fa-pencil-square-o"></core-icon> {{ 'core.vieweditor' | translate }}</button>
</div>
</div>
</div>

View File

@ -39,6 +39,11 @@ core-rich-text-editor {
font-weight: bold;
}
// Make empty elements selectable (to move the cursor).
*:empty:after {
content: '\200B';
}
ul {
list-style-type: disc;
}

View File

@ -88,6 +88,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
this.editorElement.onkeyup = this.onChange.bind(this);
this.editorElement.onpaste = this.onChange.bind(this);
this.editorElement.oninput = this.onChange.bind(this);
this.editorElement.onkeydown = this.moveCursor.bind(this);
// Listen for changes on the control to update the editor (if it is updated from outside of this component).
this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => {
@ -116,17 +117,28 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
}
}
// Use paragraph on enter.
document.execCommand('DefaultParagraphSeparator', false, 'p');
this.treatExternalContent();
this.resizeFunction = this.maximizeEditorSize.bind(this);
window.addEventListener('resize', this.resizeFunction);
setTimeout(this.resizeFunction, 1000);
let i = 0;
const interval = setInterval(() => {
const height = this.maximizeEditorSize();
if (i >= 5 || height != 0) {
clearInterval(interval);
}
i++;
}, 750);
}
/**
* Resize editor to maximize the space occupied.
*/
protected maximizeEditorSize(): void {
protected maximizeEditorSize(): number {
this.content.resize();
const contentVisibleHeight = this.content.contentHeight;
@ -138,7 +150,11 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
} else {
this.element.style.height = '';
}
return contentVisibleHeight - height;
}
return 0;
}
/**
@ -195,6 +211,132 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
this.contentChanged.emit(this.control.value);
}
/**
* On key down function to move the cursor.
* https://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div
*
* @param {Event} $event The event.
*/
moveCursor($event: Event): void {
if (!this.rteEnabled) {
return;
}
if ($event['key'] != 'ArrowLeft' && $event['key'] != 'ArrowRight') {
return;
}
$event.preventDefault();
$event.stopPropagation();
const move = $event['key'] == 'ArrowLeft' ? -1 : +1,
cursor = this.getCurrentCursorPosition(this.editorElement);
this.setCurrentCursorPosition(this.editorElement, cursor + move);
}
/**
* Returns the number of chars from the beggining where is placed the cursor.
*
* @param {Node} parent Parent where to get the position from.
* @return {number} Position in chars.
*/
protected getCurrentCursorPosition(parent: Node): number {
const selection = window.getSelection();
let charCount = -1,
node;
if (selection.focusNode) {
if (parent.contains(selection.focusNode)) {
node = selection.focusNode;
charCount = selection.focusOffset;
while (node) {
if (node.isSameNode(parent)) {
break;
}
if (node.previousSibling) {
node = node.previousSibling;
charCount += node.textContent.length;
} else {
node = node.parentNode;
if (node === null) {
break;
}
}
}
}
}
return charCount;
}
/**
* Set the caret position on the character number.
*
* @param {Node} parent Parent where to set the position.
* @param {number} [chars] Number of chars where to place the caret. If not defined it will go to the end.
*/
protected setCurrentCursorPosition(parent: Node, chars?: number): void {
/**
* Loops round all the child text nodes within the supplied node and sets a range from the start of the initial node to
* the characters.
*
* @param {Node} node Node where to start.
* @param {Range} range Previous calculated range.
* @param {any} chars Object with counting of characters (input-output param).
* @return {Range} Selection range.
*/
const setRange = (node: Node, range: Range, chars: any): Range => {
if (chars.count === 0) {
range.setEnd(node, 0);
} else if (node && chars.count > 0) {
if (node.hasChildNodes()) {
// Navigate through children.
for (let lp = 0; lp < node.childNodes.length; lp++) {
range = setRange(node.childNodes[lp], range, chars);
if (chars.count === 0) {
break;
}
}
} else if (node.textContent.length < chars.count) {
// Jump this node.
// @todo: empty nodes will be omitted.
chars.count -= node.textContent.length;
} else {
// The cursor will be placed in this element.
range.setEnd(node, chars.count);
chars.count = 0;
}
}
return range;
};
let range = document.createRange();
if (typeof chars === 'undefined') {
// Select all so it will go to the end.
range.selectNode(parent);
range.selectNodeContents(parent);
} else if (chars < 0 || chars > parent.textContent.length) {
return;
} else {
range.selectNode(parent);
range.setStart(parent, 0);
range = setRange(parent, range, {count: chars});
}
if (range) {
const selection = window.getSelection();
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
/**
* Toggle from rte editor to textarea syncing values.
*
@ -204,7 +346,8 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
$event.preventDefault();
$event.stopPropagation();
if (this.isNullOrWhiteSpace(this.control.value)) {
const isNull = this.isNullOrWhiteSpace(this.control.value);
if (isNull) {
this.clearText();
} else {
this.editorElement.innerHTML = this.control.value;
@ -217,14 +360,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
setTimeout(() => {
if (this.rteEnabled) {
this.editorElement.focus();
const range = document.createRange();
range.selectNodeContents(this.editorElement);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
this.setCurrentCursorPosition(this.editorElement.firstChild);
} else {
this.textarea.setFocus();
}
@ -279,8 +415,15 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
clearText(): void {
this.editorElement.innerHTML = '<p></p>';
this.textarea.value = '';
// Don't emit event so our valueChanges doesn't get notified by this change.
this.control.setValue(null, {emitEvent: false});
setTimeout(() => {
if (this.rteEnabled) {
this.setCurrentCursorPosition(this.editorElement);
}
}, 1);
}
/**

View File

@ -1,14 +1,28 @@
<core-loading [hideUntil]="hideUntil" class="core-loading-center">
<div class="core-tabs-bar" #topTabs [hidden]="!tabs || tabs.length < 2">
<ng-container *ngFor="let tab of tabs; let idx = index">
<a *ngIf="tab.show" [attr.aria-selected]="selected == idx" (click)="selectTab(idx)">
<core-icon *ngIf="tab.icon" [name]="tab.icon"></core-icon>
<span *ngIf="tab.title">{{ tab.title }}</span>
<ion-badge *ngIf="tab.badge" [color]="tab.badgeStyle" class="tab-badge">{{tab.badge}}</ion-badge>
</a>
</ng-container>
<ion-row>
<ion-col class="col-with-arrow" (click)="slidePrev()" no-padding col-1>
<ion-icon *ngIf="showPrevButton" name="arrow-back"></ion-icon>
</ion-col>
<ion-col no-padding col-10>
<ion-slides (ionSlideDidChange)="slideChanged()" [slidesPerView]="slidesShown">
<ng-container *ngFor="let tab of tabs; let idx = index">
<ion-slide *ngIf="tab.show">
<a [attr.aria-selected]="selected == idx" (click)="selectTab(idx)" class="tab-slide">
<core-icon *ngIf="tab.icon" [name]="tab.icon"></core-icon>
<span *ngIf="tab.title">{{ tab.title }}</span>
<ion-badge *ngIf="tab.badge" [color]="tab.badgeStyle" class="tab-badge">{{tab.badge}}</ion-badge>
</a>
</ion-slide>
</ng-container>
</ion-slides>
</ion-col>
<ion-col class="col-with-arrow" (click)="slideNext()" no-padding col-1>
<ion-icon *ngIf="showNextButton" name="arrow-forward"></ion-icon>
</ion-col>
</ion-row>
</div>
<div class="core-tabs-content-container" #originalTabs>
<ng-content></ng-content>
</div>
</core-loading>
</core-loading>

View File

@ -6,7 +6,11 @@
width: 100%;
background: $core-top-tabs-background;
> a {
.row {
width: 100%;
}
a.tab-slide {
@extend .tab-button;
background: $core-top-tabs-background;
@ -20,9 +24,25 @@
border-bottom: 2px solid $core-top-tabs-color-active !important;
}
}
ion-col {
text-align: center;
font-size: 1.6rem;
line-height: 1.6rem;
&.col-with-arrow {
display: flex;
justify-content: center;
align-items: center;
ion-icon {
color: #ccc;
}
}
}
}
.md .core-tabs-bar > a {
.md .core-tabs-bar a.tab-slide {
// @extend .tabs-md .tab-button;
min-height: $tabs-md-tab-min-height;
@ -30,17 +50,17 @@
color: $tabs-md-tab-text-color;
}
.ios .core-tabs-bar > a {
.ios .core-tabs-bar a.tab-slide {
// @extend .tabs-ios .tab-button;
max-width: $tabs-ios-tab-max-width;
min-height: $tabs-ios-tab-min-height;
font-size: $tabs-ios-tab-font-size;
font-size: $tabs-ios-tab-font-size + 4;
font-weight: $tabs-ios-tab-font-weight;
color: $tabs-ios-tab-text-color;
}
.wp .core-tabs-bar > a {
.wp .core-tabs-bar a.tab-slide {
//@extend .tabs-wp .tab-button;
@include border-radius(0);

View File

@ -17,7 +17,7 @@ import {
SimpleChange
} from '@angular/core';
import { CoreTabComponent } from './tab';
import { Content } from 'ionic-angular';
import { Content, Slides } from 'ionic-angular';
/**
* This component displays some tabs that usually share data between them.
@ -48,9 +48,16 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
@Output() ionChange: EventEmitter<CoreTabComponent> = new EventEmitter<CoreTabComponent>(); // Emitted when the tab changes.
@ViewChild('originalTabs') originalTabsRef: ElementRef;
@ViewChild('topTabs') topTabs: ElementRef;
@ViewChild(Slides) slides: Slides;
tabs: CoreTabComponent[] = []; // List of tabs.
selected: number; // Selected tab number.
showPrevButton: boolean;
showNextButton: boolean;
maxSlides = 3;
slidesShown = this.maxSlides;
numTabsShown = 0;
protected originalTabsContainer: HTMLElement; // The container of the original tabs. It will include each tab's content.
protected initialized = false;
protected afterViewInitTriggered = false;
@ -77,10 +84,16 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
*/
ngAfterViewInit(): void {
this.afterViewInitTriggered = true;
if (!this.initialized && this.hideUntil) {
// Tabs should be shown, initialize them.
this.initializeTabs();
}
window.addEventListener('resize', () => {
this.calculateMaxSlides();
this.updateSlides();
});
}
/**
@ -107,6 +120,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
if (this.getIndex(tab) == -1) {
this.tabs.push(tab);
this.sortTabs();
this.updateSlides();
if (this.initialized && this.tabs.length > 1 && this.tabBarHeight == 0) {
// Calculate the tabBarHeight again now that there is more than 1 tab and the bar will be seen.
@ -190,9 +204,72 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
}
}
// Check which arrows should be shown
this.calculateMaxSlides();
this.updateSlides();
this.initialized = true;
}
/**
* Method executed when the slides are changed.
*/
slideChanged(): void {
const currentIndex = this.slides.getActiveIndex();
if (this.slidesShown >= this.numTabsShown) {
this.showPrevButton = false;
this.showNextButton = false;
} else if (typeof currentIndex !== 'undefined') {
this.showPrevButton = currentIndex > 0;
this.showNextButton = currentIndex < this.numTabsShown - this.slidesShown;
} else {
this.showPrevButton = false;
this.showNextButton = this.numTabsShown > this.slidesShown;
}
}
/**
* Update slides.
*/
protected updateSlides(): void {
this.numTabsShown = this.tabs.reduce((prev: number, current: any) => {
return current.show ? prev + 1 : prev;
}, 0);
this.slidesShown = Math.min(this.maxSlides, this.numTabsShown);
this.slides.update();
this.slides.resize();
this.slideChanged();
}
protected calculateMaxSlides(): void {
if (this.slides && this.slides.renderedWidth) {
this.maxSlides = Math.floor(this.slides.renderedWidth / 120);
return;
}
this.maxSlides = 3;
}
/**
* Method that shows the next slide.
*/
slideNext(): void {
if (this.showNextButton) {
this.slides.slideNext();
}
}
/**
* Method that shows the previous slide.
*/
slidePrev(): void {
if (this.showPrevButton) {
this.slides.slidePrev();
}
}
/**
* Show or hide the tabs. This is used when the user is scrolling inside a tab.
*
@ -221,6 +298,8 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
removeTab(tab: CoreTabComponent): void {
const index = this.getIndex(tab);
this.tabs.splice(index, 1);
this.updateSlides();
}
/**
@ -252,6 +331,10 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
currentTab.unselectTab();
}
if (this.selected) {
this.slides.slideTo(index);
}
this.selected = index;
newTab.selectTab();
this.ionChange.emit(newTab);

View File

@ -16,10 +16,10 @@
<!-- Section selector. -->
<core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
<div text-wrap *ngIf="displaySectionSelector && sections && sections.length" no-padding class="clearfix">
<!-- @todo: How to display availabilityinfo and not visible messages? -->
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start interface="popover">
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
</ion-select>
<button float-start ion-button (click)="showSectionSelector($event)" clear class="core-button-select">
{{selectedSection && (selectedSection.formattedName || selectedSection.name) || 'core.course.sections' | translate }}
<ion-icon name="arrow-dropdown" ios="md-arrow-dropdown"></ion-icon>
</button>
<!-- Section download. -->
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
</div>

View File

@ -22,4 +22,8 @@ core-course-format {
width: 100%;
}
}
}
}
.core-section-select {
width: 100%;
}

View File

@ -15,7 +15,7 @@
import {
Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange, Output, EventEmitter, ViewChildren, QueryList, Injector
} from '@angular/core';
import { Content } from 'ionic-angular';
import { Content, ModalController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
@ -72,7 +72,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
constructor(private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, private injector: Injector,
private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider,
eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, private content: Content,
prefetchDelegate: CoreCourseModulePrefetchDelegate) {
prefetchDelegate: CoreCourseModulePrefetchDelegate, private modalCtrl: ModalController) {
this.selectOptions.title = translate.instant('core.course.sections');
this.completionChanged = new EventEmitter();
@ -221,6 +221,20 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
}
/**
* Display the section selector modal.
*/
showSectionSelector(): void {
const modal = this.modalCtrl.create('CoreCourseSectionSelectorPage',
{sections: this.sections, selected: this.selectedSection});
modal.onDidDismiss((newSection) => {
if (newSection) {
this.sectionChanged(newSection);
}
});
modal.present();
}
/**
* Function called when selected section changes.
*

View File

@ -1,6 +1,8 @@
<ion-card *ngIf="description">
<ion-item text-wrap>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" [maxHeight]="showFull && showFull !== 'false' ? 0 : 120" fullOnClick="true"></core-format-text>
<ion-note *ngIf="note" item-end>{{ note }}</ion-note>
</ion-item>
<ion-item text-wrap *ngIf="note">
<ion-note item-end>{{ note }}</ion-note>
</ion-item>
</ion-card>

View File

@ -11,6 +11,7 @@
"contents": "Contents",
"couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
"couldnotloadsections": "Could not load the sections. Please try again later.",
"coursesummary": "Course summary",
"downloadcourse": "Download course",
"errordownloadingcourse": "Error downloading course.",
"errordownloadingsection": "Error downloading section.",

View File

@ -0,0 +1,19 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'core.course.sections' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ng-container *ngFor="let section of sections">
<ion-item *ngIf="sectionHasContent(section)" text-wrap (click)="selectSection(section)" [class.core-primary-item]="selected.id == section.id" [class.item-dimmed]="section.visible === 0 || section.uservisible === false">
<h2><core-format-text [text]="section.formattedName || section.name"></core-format-text></h2>
<ion-badge color="secondary" *ngIf="section.visible === 0">{{ 'core.course.nocontentavailable' | translate }}</ion-badge>
<ion-badge color="secondary" *ngIf="section.availabilityinfo"><core-format-text [text]=" section.availabilityinfo"></core-format-text></ion-badge>
</ion-item>
</ng-container>
</ion-content>

View File

@ -0,0 +1,33 @@
// (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 { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseSectionSelectorPage } from './section-selector';
@NgModule({
declarations: [
CoreCourseSectionSelectorPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreCourseSectionSelectorPage),
TranslateModule.forChild()
],
})
export class CoreCourseSectionSelectorPageModule {}

View File

@ -0,0 +1,57 @@
// (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 { IonicPage, NavParams, ViewController } from 'ionic-angular';
import { CoreCourseHelperProvider } from '../../providers/helper';
/**
* Page that displays course section selector.
*/
@IonicPage({ segment: 'core-course-section-selector' })
@Component({
selector: 'page-core-course-section-selector',
templateUrl: 'section-selector.html',
})
export class CoreCourseSectionSelectorPage {
sections: any;
selected: number;
sectionHasContent: any;
constructor(navParams: NavParams, courseHelper: CoreCourseHelperProvider, private viewCtrl: ViewController) {
this.sections = navParams.get('sections');
this.selected = navParams.get('selected');
this.sectionHasContent = courseHelper.sectionHasContent;
}
/**
* Close the modal.
*/
closeModal(): void {
this.viewCtrl.dismiss();
}
/**
* Select a section.
*
* @param {any} section Selected section object.
*/
selectSection(section: any): void {
if (!(section.visible === 0 || section.uservisible === false)) {
this.viewCtrl.dismiss(section);
}
}
}

View File

@ -14,6 +14,7 @@
<core-context-menu>
<core-context-menu-item *ngIf="displayEnableDownload" [priority]="2000" [content]="'core.settings.enabledownloadsection' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
<core-context-menu-item [hidden]="!downloadCourseEnabled" [priority]="1900" [content]="prefetchCourseData.title | translate" (action)="prefetchCourse()" [iconAction]="prefetchCourseData.prefetchCourseIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item [priority]="1800" [content]="'core.course.coursesummary' | translate" (action)="openCourseSummary()" iconAction="fa-graduation-cap"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-content>

View File

@ -356,6 +356,13 @@ export class CoreCourseSectionPage implements OnDestroy {
this.prefetchCourseData.title = statusData.title;
}
/**
* Open the course summary
*/
openCourseSummary(): void {
this.navCtrl.push('CoreCoursesCoursePreviewPage', {course: this.course, avoidOpenCourse: true});
}
/**
* Page destroyed.
*/

View File

@ -16,6 +16,7 @@ import { Component, Input, OnInit, Optional } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreCoursesProvider } from '../../providers/courses';
import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate';
/**
* This directive is meant to display an item for a list of courses.
@ -32,7 +33,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
@Input() course: any; // The course to render.
constructor(@Optional() private navCtrl: NavController, private translate: TranslateService,
private coursesProvider: CoreCoursesProvider) {
private coursesProvider: CoreCoursesProvider, private courseFormatDelegate: CoreCourseFormatDelegate) {
}
/**
@ -80,6 +81,10 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
* @param {any} course The course to open.
*/
openCourse(course: any): void {
this.navCtrl.push('CoreCoursesCoursePreviewPage', {course: course});
if (course.isEnrolled) {
this.courseFormatDelegate.openCourse(this.navCtrl, course);
} else {
this.navCtrl.push('CoreCoursesCoursePreviewPage', {course: course});
}
}
}

View File

@ -13,7 +13,7 @@
<div *ngIf="course.imageThumb" (click)="openCourse()" class="core-course-thumb">
<img [src]="course.imageThumb" core-external-content alt=""/>
</div>
<a ion-item text-wrap (click)="openCourse()" [title]="course.fullname" [attr.detail-none]="!canAccessCourse">
<a ion-item text-wrap (click)="openCourse()" [title]="course.fullname" [attr.detail-none]=" avoidOpenCourse || !canAccessCourse">
<core-icon name="fa-graduation-cap" fixed-width item-start></core-icon>
<h2><core-format-text [text]="course.fullname"></core-format-text></h2>
<p *ngIf="course.categoryname"><core-format-text [text]="course.categoryname"></core-format-text></p>
@ -26,7 +26,12 @@
<ng-container text-wrap *ngIf="course.contacts && course.contacts.length">
<ion-item-divider color="light">{{ 'core.teachers' | translate }}</ion-item-divider>
<a ion-item text-wrap *ngFor="let contact of course.contacts" core-user-link userId="{{contact.id}}" courseId="{{isEnrolled ? course.id : null}}" [attr.aria-label]="'core.viewprofile' | translate">{{contact.fullname}}</a>
<a ion-item text-wrap *ngFor="let contact of course.contacts" core-user-link userId="{{contact.id}}" courseId="{{isEnrolled ? course.id : null}}" [attr.aria-label]="'core.viewprofile' | translate">
<ion-avatar item-start>
<img [src]="contact.userpictureurl" onError="this.src='assets/img/user-avatar.png'" core-external-content [alt]="'core.pictureof' | translate:{$a: contact.userfullname}" role="presentation">
</ion-avatar>
<h2>{{contact.fullname}}</h2>
</a>
<ion-item-divider color="light"></ion-item-divider>
</ng-container>
<core-file *ngFor="let file of course.overviewfiles" [file]="file" [component]="component" [componentId]="course.id"></core-file>
@ -49,7 +54,7 @@
<ion-spinner *ngIf="prefetchCourseData.prefetchCourseIcon == 'spinner'" item-start></ion-spinner>
<h2>{{ 'core.course.downloadcourse' | translate }}</h2>
</a>
<a ion-item (click)="openCourse()" [title]="course.fullname" *ngIf="canAccessCourse">
<a ion-item (click)="openCourse()" [title]="course.fullname" *ngIf="!avoidOpenCourse && canAccessCourse">
<ion-icon name="briefcase" item-start></ion-icon>
<h2>{{ 'core.course.contents' | translate }}</h2>
</a>

View File

@ -24,6 +24,7 @@ import { CoreCoursesProvider } from '../../providers/courses';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate';
/**
* Page that allows "previewing" a course and enrolling in it if enabled and not enrolled.
@ -41,6 +42,7 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
selfEnrolInstances: any[] = [];
paypalEnabled: boolean;
dataLoaded: boolean;
avoidOpenCourse = false;
prefetchCourseData = {
prefetchCourseIcon: 'spinner',
title: 'core.course.downloadcourse'
@ -67,9 +69,10 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
private coursesProvider: CoreCoursesProvider, private platform: Platform, private modalCtrl: ModalController,
private translate: TranslateService, private eventsProvider: CoreEventsProvider,
private courseOptionsDelegate: CoreCourseOptionsDelegate, private courseHelper: CoreCourseHelperProvider,
private courseProvider: CoreCourseProvider) {
private courseProvider: CoreCourseProvider, private courseFormatDelegate: CoreCourseFormatDelegate) {
this.course = navParams.get('course');
this.avoidOpenCourse = navParams.get('avoidOpenCourse');
this.isMobile = appProvider.isMobile();
this.isDesktop = appProvider.isDesktop();
this.downloadCourseEnabled = !this.coursesProvider.isDownloadCourseDisabledInSite();
@ -224,11 +227,7 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
}).catch(() => {
// The user is not an admin/manager. Check if we can provide guest access to the course.
return this.canAccessAsGuest().then((passwordRequired) => {
if (!passwordRequired) {
this.canAccessCourse = true;
} else {
this.canAccessCourse = false;
}
this.canAccessCourse = !passwordRequired;
}).catch(() => {
this.canAccessCourse = false;
});
@ -242,12 +241,12 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
* Open the course.
*/
openCourse(): void {
if (!this.canAccessCourse) {
// Course cannot be opened.
if (!this.canAccessCourse || this.avoidOpenCourse) {
// Course cannot be opened or we are avoiding opening because we accessed from inside a course.
return;
}
this.navCtrl.push('CoreCourseSectionPage', { course: this.course });
this.courseFormatDelegate.openCourse(this.navCtrl, this.course);
}
/**

View File

@ -1,10 +1,12 @@
page-core-login-init {
.core-bglogo {
.scroll-content {
background-color: $core-color-init-screen; /* Change this to add a bg image or change color */
background: -webkit-radial-gradient($core-color-init-screen-alt, $core-color-init-screen);
background: radial-gradient($core-color-init-screen-alt, $core-color-init-screen);
background-repeat: no-repeat;
background-position: center center;
}
.core-bglogo {
position: absolute;
top: 0;
bottom: 0;

View File

@ -26,9 +26,8 @@
<!-- Pick the site from a list of fixed sites. -->
<div *ngIf="fixedSites" text-wrap>
<!-- Display them using a select. -->
<ion-item *ngIf="!displayAsButtons">
<!-- @todo: Display label and select in different lines. -->
<ion-label for="siteSelect">{{ 'core.login.selectsite' | translate }}</ion-label>
<ion-item *ngIf="!displayAsButtons" margin-vertical>
<ion-label stacked for="siteSelect">{{ 'core.login.selectsite' | translate }}</ion-label>
<ion-select formControlName="siteUrl" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" interface="popover">
<ion-option *ngFor="let site of fixedSites" [value]="site.url">{{site.name}}</ion-option>
</ion-select>
@ -37,7 +36,7 @@
<!-- Display them using buttons. -->
<div *ngIf="displayAsButtons">
<p class="padding no-padding-bottom">{{ 'core.login.selectsite' | translate }}</p>
<a *ngFor="let site of fixedSites" ion-button block (click)="connect(site.url)" title="{{site.name}}">{{site.name}}</a>
<a *ngFor="let site of fixedSites" ion-button block (click)="connect(site.url)" title="{{site.name}}" margin-bottom>{{site.name}}</a>
</div>
</div>

View File

@ -9,7 +9,7 @@
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-content class="has-fab">
<ion-list>
<ion-item (click)="login(site.id)" *ngFor="let site of sites; let idx = index">
<ion-avatar item-start>

View File

@ -22,7 +22,10 @@
"@components/*": ["components/*"],
"@directives/*": ["directives/*"],
"@pipes/*": ["pipes/*"]
}
},
"typeRoots": [
"node_modules/@types"
]
},
"include": [
"src/**/*.ts"