commit
4a70a972e2
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ core-empty-box {
|
|||
display: table-cell;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.core-empty-box-inline {
|
||||
|
|
|
@ -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>"><pre></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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -22,4 +22,8 @@ core-course-format {
|
|||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-section-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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>
|
|
@ -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.",
|
||||
|
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -22,7 +22,10 @@
|
|||
"@components/*": ["components/*"],
|
||||
"@directives/*": ["directives/*"],
|
||||
"@pipes/*": ["pipes/*"]
|
||||
}
|
||||
},
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
|
|
Loading…
Reference in New Issue