Merge pull request #2353 from crazyserver/MOBILE-3402

MOBILE-3402 login: Improve login page UX
main
Juan Leyva 2020-04-28 17:00:27 +02:00 committed by GitHub
commit eb9ba11d39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 594 additions and 438 deletions

View File

@ -46,19 +46,6 @@ ion-app.app-root {
text-transform: none;
}
@include media-breakpoint-up(sm) {
.core-center-view .scroll-content {
display: flex!important;
align-content: center !important;
align-items: center !important;
> * {
margin: 0 auto;
width: 100%;
max-width: 600px;
}
}
}
@include media-breakpoint-down(sm) {
.hidden-phone {
display: none !important;

View File

@ -1332,7 +1332,9 @@
"core.block.blocks": "Blocks",
"core.browser": "Browser",
"core.cancel": "Cancel",
"core.cannotconnect": "<strong>Cannot connect</strong>: Verify that you have correctly typed your site address.",
"core.cannotconnect": "Cannot connect",
"core.cannotconnecttrouble": "We're having trouble connecting to your site.",
"core.cannotconnectverify": "<strong>Please check the address is correct.</strong>",
"core.cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.",
"core.captureaudio": "Record audio",
"core.capturedimage": "Taken picture.",
@ -1759,8 +1761,6 @@
"core.login.policyagreement": "Site policy agreement",
"core.login.policyagreementclick": "Link to site policy agreement",
"core.login.potentialidps": "Log in using your account on:",
"core.login.problemconnectingerror": "We're having trouble connecting to",
"core.login.problemconnectingerrorcontinue": "Double check you've entered the address correctly and try again.",
"core.login.profileinvaliddata": "Invalid value",
"core.login.recaptchachallengeimage": "reCAPTCHA challenge image",
"core.login.recaptchaexpired": "Verification expired. Answer the security question again.",
@ -1774,7 +1774,7 @@
"core.login.selectacountry": "Select a country",
"core.login.selectsite": "Please select your site:",
"core.login.signupplugindisabled": "{{$a}} is not enabled.",
"core.login.siteaddress": "Your site address",
"core.login.siteaddress": "Your site",
"core.login.sitehasredirect": "Your site contains at least one HTTP redirect. The app cannot follow redirects, this could be the issue that's preventing the app from connecting to your site.",
"core.login.siteinmaintenance": "Your site is in maintenance mode",
"core.login.sitepolicynotagreederror": "Site policy not agreed.",
@ -1788,7 +1788,8 @@
"core.login.usernamerequired": "Username required",
"core.login.usernotaddederror": "User not added - error",
"core.login.visitchangepassword": "Do you want to visit the site to change the password?",
"core.login.webservicesnotenabled": "Web services are not enabled in your site. Please contact your site administrator if you think they should be enabled.",
"core.login.webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.",
"core.login.yourenteredsite": "Connect to your site",
"core.lostconnection": "Your authentication token is invalid or has expired. You will have to reconnect to the site.",
"core.mainmenu.changesite": "Change site",
"core.mainmenu.help": "Help",

View File

@ -73,8 +73,6 @@
"policyagreement": "Site policy agreement",
"policyagreementclick": "Link to site policy agreement",
"potentialidps": "Log in using your account on:",
"problemconnectingerror": "We're having trouble connecting to",
"problemconnectingerrorcontinue": "Double check you've entered the address correctly and try again.",
"profileinvaliddata": "Invalid value",
"recaptchachallengeimage": "reCAPTCHA challenge image",
"recaptchaexpired": "Verification expired. Answer the security question again.",
@ -88,7 +86,7 @@
"selectacountry": "Select a country",
"selectsite": "Please select your site:",
"signupplugindisabled": "{{$a}} is not enabled.",
"siteaddress": "Your site address",
"siteaddress": "Your site",
"sitehasredirect": "Your site contains at least one HTTP redirect. The app cannot follow redirects, this could be the issue that's preventing the app from connecting to your site.",
"siteinmaintenance": "Your site is in maintenance mode",
"sitepolicynotagreederror": "Site policy not agreed.",
@ -102,5 +100,6 @@
"usernamerequired": "Username required",
"usernotaddederror": "User not added - error",
"visitchangepassword": "Do you want to visit the site to change the password?",
"webservicesnotenabled": "Web services are not enabled in your site. Please contact your site administrator if you think they should be enabled."
"yourenteredsite": "Connect to your site",
"webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help."
}

View File

@ -1,11 +1,28 @@
$core-login-page-background-color: $white !default;
$core-login-page-text-color: $text-color !default;
$core-login-button-outline: false !default;
$core-login-loading-color: false !default;
$core-login-item-inner-background-color: $white !default;
$core-login-item-background-color: $white !default;
// Dark.
$core-dark-login-page-background-color: $black !default;
$core-dark-login-page-text-color: $core-dark-text-color !default;
$core-dark-login-item-inner-background-color: $core-dark-login-page-background-color !default;
$core-dark-login-item-background-color: $core-dark-login-page-background-color !default;
$core-dark-login-button-outline: $core-login-button-outline !default;
$core-dark-login-loading-color: $core-dark-text-color !default;
ion-app.app-root page-core-login-credentials,
ion-app.app-root page-core-login-reconnect,
ion-app.app-root page-core-login-site {
.scroll-content {
background: $core-login-page-background-color;
color: $core-login-page-text-color;
@include darkmode() {
background: $core-dark-login-page-background-color;
color: $core-dark-login-page-text-color;
}
}
@ -13,35 +30,18 @@ ion-app.app-root page-core-login-site {
max-width: 100%;
}
img.login-logo {
width: 90%;
max-width: 300px;
}
.box {
padding: 16px;
margin: 8px;
background: $core-login-box-background-color;
border: 1px solid $core-login-box-background-border;
color: $core-login-box-text-color;
@include darkmode() {
background: $core-dark-login-box-background-color;
border-color: $core-dark-login-box-background-border;
color: $core-dark-login-box-text-color;
}
.item {
@include darkmode() {
background: $core-dark-login-box-background-color;
}
}
}
.core-sitename, .core-siteurl {
@if $core-fixed-url { display: none; }
}
.core-sitename + .core-siteurl {
margin-top: 0;
}
.core-sitename {
font-size: 1.8rem;
}
@if $core-login-button-outline {
.button-md.button-default-md, .button-ios.button-default-ios {
@extend .button-md-light;
@ -77,4 +77,27 @@ ion-app.app-root page-core-login-site {
.item-input {
margin-bottom: 20px;
}
ion-list.core-login-forgotten-password {
margin-top: 0;
margin-bottom: 0;
a.item {
background: transparent;
text-decoration: underline;
@include darkmode() {
background: transparent;
}
}
}
.core-login-site-logo {
margin-top: 5px;
margin-bottom: 5px;
img {
width: 90%;
max-width: 300px;
}
}
}

View File

@ -8,7 +8,7 @@
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding class="core-center-view">
<ion-content padding>
<ion-list>
<ion-item text-wrap *ngIf="!changingPassword">
<h2>{{ 'core.login.forcepasswordchangenotice' | translate }}</h2>

View File

@ -9,52 +9,54 @@
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content class="core-center-view">
<ion-content padding>
<core-loading [hideUntil]="pageLoaded">
<div class="box">
<div text-wrap text-center margin-bottom>
<div text-wrap text-center margin-bottom>
<div class="core-login-site-logo">
<!-- Show site logo or a default image. -->
<img *ngIf="logoUrl" [src]="logoUrl" role="presentation">
<img *ngIf="!logoUrl" src="assets/img/login_logo.png" class="login-logo" role="presentation">
<!-- If no sitename show big siteurl. -->
<p *ngIf="!siteName" padding class="item-heading core-siteurl">{{siteUrl}}</p>
<!-- If sitename, show big sitename and small siteurl. -->
<p *ngIf="siteName" padding class="item-heading core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></p>
<p *ngIf="siteName" class="core-siteurl">{{siteUrl}}</p>
</div>
<form ion-list [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #credentialsForm>
<ion-item *ngIf="siteChecked && !isBrowserSSO">
<ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}" formControlName="username" autocapitalize="none" autocorrect="off"></ion-input>
</ion-item>
<ion-item *ngIf="siteChecked && !isBrowserSSO" margin-bottom>
<core-show-password item-content [name]="'password'">
<ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-show-password [clearOnEdit]="false"></ion-input>
</core-show-password>
</ion-item>
<button ion-button block [disabled]="siteChecked && !isBrowserSSO && !credForm.valid">{{ 'core.login.loginbutton' | translate }}</button>
</form>
<!-- Forgotten password button. -->
<div *ngIf="showForgottenPassword" padding-top class="core-login-forgotten-password">
<button ion-button block text-wrap color="light" (click)="forgottenPassword()">{{ 'core.login.forgotten' | translate }}</button>
<img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation">
</div>
<ion-list *ngIf="identityProviders && identityProviders.length" padding-top class="core-login-identity-providers">
<ion-list-header text-wrap>{{ 'core.login.potentialidps' | translate }}</ion-list-header>
<button ion-item *ngFor="let provider of identityProviders" text-wrap class="core-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}">
<img [src]="provider.iconurl" alt="" width="32" height="32" item-start>
{{provider.name}}
</button>
</ion-list>
<ion-list *ngIf="canSignup" padding-top class="core-login-sign-up">
<ion-list-header text-wrap>{{ 'core.login.firsttime' | translate }}</ion-list-header>
<ion-item no-lines text-wrap *ngIf="authInstructions">
<p><core-format-text [text]="authInstructions" [filter]="false"></core-format-text></p>
</ion-item>
<button ion-button block (click)="signup()">{{ 'core.login.startsignup' | translate }}</button>
</ion-list>
<h3 *ngIf="siteName" padding class="core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></h3>
<p class="core-siteurl">{{siteUrl}}</p>
</div>
<form ion-list [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #credentialsForm>
<ion-item *ngIf="siteChecked && !isBrowserSSO">
<ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}" formControlName="username" autocapitalize="none" autocorrect="off"></ion-input>
</ion-item>
<ion-item *ngIf="siteChecked && !isBrowserSSO" margin-bottom>
<core-show-password item-content [name]="'password'">
<ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-show-password [clearOnEdit]="false"></ion-input>
</core-show-password>
</ion-item>
<div padding>
<button ion-button block [disabled]="siteChecked && !isBrowserSSO && !credForm.valid" class="core-login-login-button">{{ 'core.login.loginbutton' | translate }}</button>
</div>
</form>
<!-- Forgotten password button. -->
<ion-list no-lines *ngIf="showForgottenPassword" class="core-login-forgotten-password">
<a ion-item text-center text-wrap (click)="forgottenPassword()" detail-none>
{{ 'core.login.forgotten' | translate }}
</a>
</ion-list>
<ion-list *ngIf="identityProviders && identityProviders.length" padding-top class="core-login-identity-providers">
<ion-item text-wrap no-lines><h3 class="item-heading">{{ 'core.login.potentialidps' | translate }}</h3></ion-item>
<button ion-item *ngFor="let provider of identityProviders" text-wrap class="core-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}">
<img [src]="provider.iconurl" alt="" width="32" height="32" item-start>
{{provider.name}}
</button>
</ion-list>
<ion-list *ngIf="canSignup" padding-top class="core-login-sign-up">
<ion-item text-wrap no-lines><h3 class="item-heading">{{ 'core.login.firsttime' | translate }}</h3></ion-item>
<ion-item no-lines text-wrap *ngIf="authInstructions">
<p><core-format-text [text]="authInstructions" [filter]="false"></core-format-text></p>
</ion-item>
<button ion-button block color="light" (click)="signup()">{{ 'core.login.startsignup' | translate }}</button>
</ion-list>
</core-loading>
</ion-content>

View File

@ -1,5 +0,0 @@
ion-app.app-root page-core-login-credentials {
.item-input {
margin-bottom: 20px;
}
}

View File

@ -65,6 +65,8 @@ export class CoreLoginCredentialsPage {
private eventsProvider: CoreEventsProvider) {
this.siteUrl = navParams.get('siteUrl');
this.siteName = navParams.get('siteName') || null;
this.logoUrl = navParams.get('logoUrl') || null;
this.siteConfig = navParams.get('siteConfig');
this.urlToOpen = navParams.get('urlToOpen');
@ -170,8 +172,6 @@ export class CoreLoginCredentialsPage {
this.eventsProvider.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, { config: this.siteConfig });
}
} else {
this.siteName = null;
this.logoUrl = null;
this.authInstructions = null;
this.canSignup = false;
this.identityProviders = [];

View File

@ -3,72 +3,69 @@
<ion-title>{{ 'core.login.reconnect' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content class="core-center-view">
<div class="box">
<div *ngIf="site" text-wrap text-center margin-bottom [ngClass]="{'item-avatar-center': showSiteAvatar}">
<ng-container *ngIf="showSiteAvatar">
<ion-avatar>
<!-- Show user avatar. -->
<img [src]="site.avatar" class="avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullname} }}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
</ng-container>
<ion-content padding>
<div *ngIf="site" text-wrap text-center margin-bottom [ngClass]="{'item-avatar-center': showSiteAvatar}">
<ng-container *ngIf="showSiteAvatar">
<ion-avatar>
<!-- Show user avatar. -->
<img [src]="site.avatar" class="avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullname} }}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
</ng-container>
<ng-container *ngIf="!showSiteAvatar">
<!-- Show site logo or a default image. -->
<img *ngIf="logoUrl" [src]="logoUrl" core-external-content [siteId]="site.id" role="presentation">
<img *ngIf="!logoUrl" src="assets/img/login_logo.png" class="login-logo" role="presentation">
</ng-container>
<!-- If no sitename show big siteurl. -->
<p *ngIf="!siteName" class="item-heading core-siteurl">{{siteUrl}}</p>
<!-- If sitename, show big sitename and small siteurl. -->
<p *ngIf="siteName" class="item-heading core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></p>
<p *ngIf="siteName" class="core-siteurl">{{siteUrl}}</p>
<p *ngIf="!isLoggedOut">
<ion-icon padding name="alert"></ion-icon> {{ 'core.login.reconnectdescription' | translate }}
</p>
</div>
<form ion-list *ngIf="!isOAuth" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm>
<ion-item text-wrap class="core-username">
<p>{{username}}</p>
</ion-item>
<ion-item margin-bottom>
<core-show-password item-content [name]="'password'">
<ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"></ion-input>
</core-show-password>
</ion-item>
<ion-grid no-padding>
<ion-row>
<ion-col>
<a ion-button block color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</a>
</ion-col>
<ion-col>
<button ion-button block [disabled]="!credForm.valid">{{ 'core.login.loginbutton' | translate }}</button>
</ion-col>
</ion-row>
</ion-grid>
</form>
<!-- Forgotten password button. -->
<div *ngIf="showForgottenPassword && !isOAuth" padding-top class="core-login-forgotten-password">
<button ion-button block text-wrap color="light" (click)="forgottenPassword()">{{ 'core.login.forgotten' | translate }}</button>
<div class="core-login-site-logo" *ngIf="!showSiteAvatar">
<!-- Show site logo or a default image. -->
<img *ngIf="logoUrl" [src]="logoUrl" core-external-content [siteId]="siteId" role="presentation">
<img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation">
</div>
<!-- Identity providers. -->
<ion-list *ngIf="identityProviders && identityProviders.length" padding-top class="core-login-identity-providers">
<ion-list-header text-wrap>{{ 'core.login.potentialidps' | translate }}</ion-list-header>
<button ion-item *ngFor="let provider of identityProviders" text-wrap class="core-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}">
<img [src]="provider.iconurl" alt="" width="32" height="32" item-start>
{{provider.name}}
</button>
</ion-list>
<h3 *ngIf="siteName" padding class="core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></h3>
<p class="core-siteurl">{{siteUrl}}</p>
<!-- If OAuth, display cancel button since the form isn't displayed. -->
<ion-list *ngIf="isOAuth">
<ion-item>
<a ion-button block color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</a>
</ion-item>
</ion-list>
<p *ngIf="!isLoggedOut" class="core-login-reconnect-warning">
<ion-icon padding name="alert"></ion-icon> {{ 'core.login.reconnectdescription' | translate }}
</p>
</div>
<form ion-list *ngIf="!isOAuth" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm>
<ion-item text-wrap class="core-username">
<p>{{username}}</p>
</ion-item>
<ion-item margin-bottom>
<core-show-password item-content [name]="'password'">
<ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"></ion-input>
</core-show-password>
</ion-item>
<ion-grid padding>
<ion-row>
<ion-col>
<a ion-button block color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</a>
</ion-col>
<ion-col>
<button ion-button block [disabled]="!credForm.valid">{{ 'core.login.loginbutton' | translate }}</button>
</ion-col>
</ion-row>
</ion-grid>
</form>
<!-- Forgotten password button. -->
<ion-list no-lines *ngIf="showForgottenPassword && !isOAuth" class="core-login-forgotten-password">
<a ion-item text-center text-wrap (click)="forgottenPassword()" detail-none>
{{ 'core.login.forgotten' | translate }}
</a>
</ion-list>
<!-- Identity providers. -->
<ion-list *ngIf="identityProviders && identityProviders.length" padding-top class="core-login-identity-providers">
<ion-item text-wrap><h3 class="item-heading">{{ 'core.login.potentialidps' | translate }}</h3></ion-item>
<button ion-item *ngFor="let provider of identityProviders" text-wrap class="core-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}">
<img [src]="provider.iconurl" alt="" width="32" height="32" item-start>
{{provider.name}}
</button>
</ion-list>
<!-- If OAuth, display cancel button since the form isn't displayed. -->
<ion-list *ngIf="isOAuth">
<ion-item>
<a ion-button block color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</a>
</ion-item>
</ion-list>
</ion-content>

View File

@ -30,7 +30,7 @@ ion-app.app-root page-core-login-reconnect {
}
}
.item-input {
margin-bottom: 20px;
.core-login-reconnect-warning {
color: $red;
}
}

View File

@ -1,27 +0,0 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'core.error' | 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 padding>
<h3>{{ 'core.whoops' | translate }}</h3>
<p>{{ 'core.login.problemconnectingerror' | translate }}</p>
<p padding>{{siteUrl}}</p>
<p>{{ 'core.login.problemconnectingerrorcontinue' | translate }}</p>
<button ion-button block (click)="closeModal()">{{ 'core.tryagain' | translate }}</button>
<h3>{{ 'core.login.stillcantconnect' | translate }}</h3>
<p>{{ 'core.login.contactyouradministrator' | translate }}</p>
<p *ngIf="issue">
{{ 'core.login.contactyouradministratorissue' | translate:{$a: ''} }}
</p>
<p *ngIf="issue" margin-bottom>
<core-format-text [text]="issue" [filter]="false"></core-format-text>
</p>
</ion-content>

View File

@ -1,31 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreLoginSiteErrorPage } from './site-error';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
CoreLoginSiteErrorPage
],
imports: [
CoreDirectivesModule,
IonicPageModule.forChild(CoreLoginSiteErrorPage),
TranslateModule.forChild()
]
})
export class CoreLoginSiteErrorPageModule {}

View File

@ -1,3 +0,0 @@
page-core-login-site-error button.button.button-block {
margin-bottom: 3rem;
}

View File

@ -1,41 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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, ViewController, NavParams } from 'ionic-angular';
/**
* Component that displays an error when trying to connect to a site.
*/
@IonicPage({ segment: 'core-login-site-error' })
@Component({
selector: 'page-core-login-site-error',
templateUrl: 'site-error.html',
})
export class CoreLoginSiteErrorPage {
siteUrl: string;
issue: string;
constructor(private viewCtrl: ViewController, params: NavParams) {
this.siteUrl = params.get('siteUrl');
this.issue = params.get('issue');
}
/**
* Close modal.
*/
closeModal(): void {
this.viewCtrl.dismiss();
}
}

View File

@ -9,9 +9,9 @@
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content class="core-center-view">
<div class="box">
<div text-center padding>
<ion-content padding>
<div>
<div text-center padding margin-bottom [class.hidden]="hasSites" class="core-login-site-logo">
<img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation">
</div>
<form ion-list [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites || fixedDisplay == 'select'" #siteFormEl>
@ -19,19 +19,36 @@
<ng-container *ngIf="!fixedSites">
<ion-item>
<ion-label stacked><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label>
<ion-input type="url" name="url" placeholder="https://campus.example.edu" formControlName="siteUrl" [core-auto-focus]="showKeyboard"></ion-input>
<ion-input type="url" name="url" placeholder="https://campus.example.edu" formControlName="siteUrl" [core-auto-focus]="showKeyboard" (ionChange)="searchSite($event, siteForm.value.siteUrl)"></ion-input>
</ion-item>
</ng-container>
<ion-list *ngIf="!fixedSites" [class.hidden]="!hasSites" class="core-login-site-list" [class.dimmed]="loadingSites">
<div *ngIf="loadingSites" class="core-login-site-list-loading"><ion-spinner></ion-spinner></div>
<ion-item no-lines class="core-login-site-list-title" *ngIf="onlyWrittenSite"><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item>
<ion-item *ngFor="let site of sites" (click)="connect($event, site.url, site)" [title]="site.name" detail-push [class.core-login-entered-site]="!site.fromWS">
<ion-thumbnail item-start>
<core-icon name="fa-pencil" *ngIf="!site.imageurl && !site.fromWS"></core-icon>
<img [src]="site.imageurl" *ngIf="site.imageurl">
<img src="assets/icon/icon.png" *ngIf="!site.imageurl && site.fromWS" class="core-login-default-icon">
</ion-thumbnail>
<h2 text-wrap>{{site.name}}<ng-container *ngIf="site.alias"> ({{site.alias}})</ng-container></h2>
<p>{{site.noProtocolUrl}}</p>
<p *ngIf="site.country || site.city" text-wrap><ng-container *ngIf="site.city">{{site.city}} - </ng-container>{{site.country}}</p>
</ion-item>
</ion-list>
<div *ngIf="!fixedSites && !hasSites && loadingSites" class="core-login-site-nolist-loading"><ion-spinner></ion-spinner></div>
<!-- Pick the site from a list of fixed sites. -->
<ion-item *ngIf="fixedSites && fixedDisplay == 'select'" margin-vertical text-wrap>
<ion-label stacked for="siteSelect">{{ 'core.login.selectsite' | translate }}</ion-label>
<ion-select formControlName="siteUrl" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" interface="action-sheet">
<ion-option *ngFor="let site of fixedSites" [value]="site.url">{{site.name}}</ion-option>
</ion-select>
</ion-item>
<button ion-button block [disabled]="!siteForm.valid">{{ 'core.login.connect' | translate }}</button>
</form>
<!-- Pick the site from a list of fixed sites. -->
<ion-list *ngIf="fixedSites && (fixedDisplay == 'list' || fixedDisplay == 'listnourl')">
<ion-item no-lines><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item>
@ -48,33 +65,9 @@
<a *ngFor="let site of fixedSites" ion-button block (click)="connect($event, site.url)" [title]="site.name" margin-bottom>{{site.name}}</a>
</div>
<!-- Error. -->
<div padding-top *ngIf="error" >
<ion-card class="core-site-error">
<ion-card-header>
{{ 'core.whoops' | translate }}
</ion-card-header>
<ion-card-content>
<p><core-format-text [text]="error.message" [filter]="false"></core-format-text></p>
<ng-container *ngIf="error.url">
<p>{{ 'core.login.problemconnectingerror' | translate }}</p>
<p padding><a [href]="error.fullUrl" core-link>{{ error.url }}</a></p>
<p><strong>{{ 'core.login.problemconnectingerrorcontinue' | translate }}</strong></p>
</ng-container>
</ion-card-content>
<ion-card-header>
{{ 'core.login.stillcantconnect' | translate }}
</ion-card-header>
<ion-card-content>
<p>{{ 'core.login.contactyouradministrator' | translate }}</p>
<p>{{ 'core.whoissiteadmin' | translate }}</p>
</ion-card-content>
</ion-card>
</div>
<!-- Help. -->
<ion-list no-lines>
<a ion-item text-center class="core-login-need-help" (click)="showHelp()" detail-none>
<ion-list no-lines margin-top>
<a ion-item text-center text-wrap class="core-login-need-help" (click)="showHelp()" detail-none>
{{ 'core.needhelp' | translate }}
</a>
</ion-list>

View File

@ -11,23 +11,108 @@ ion-app.app-root page-core-login-site {
}
}
.core-site-error {
background: $red-light;
margin-left: 0;
margin-right: 0;
width: 100%;
user-select: text;
p, ion-card-header {
color: $red-dark;
user-select: text;
}
ion-card-header {
font-weight: bold;
}
}
.core-login-need-help {
.core-login-need-help.item {
background: transparent;
text-decoration: underline;
@include darkmode() {
background: transparent;
}
}
}
.core-login-site-connect {
margin-top: 1.4rem;
}
.item ion-thumbnail {
min-width: 50px;
min-height: 50px;
border-radius: 20%;
box-shadow: 0 0 4px #eee;
text-align: center;
img {
width: 50px;
height: 50px;
}
ion-icon {
margin: 0 auto;
font-size: 40px;
line-height: 50px;
}
}
.core-login-site-logo,
.core-login-site-list {
transition-delay: 0s;
visibility: visible;
opacity: 1;
transition: all 0.7s ease-in-out;
max-height: 9999px;
&.hidden {
opacity: 0;
visibility: hidden;
margin: 0;
padding: 0;
max-height: 0;
}
}
.core-login-site-list.dimmed {
pointer-events: none;
position: relative;
}
.core-login-site-list-loading {
position: absolute;
@include position(0, 0, 0, 0);
width: 100%;
height: 100%;
display: flex;
align-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.5);
z-index: 1;
ion-spinner {
flex: 1;
}
}
.core-login-site-nolist-loading {
text-align: center;
}
.item.core-login-site-list-title {
ion-label, ion-label h2.item-heading {
margin-top: 0;
}
}
@include media-breakpoint-up(md) {
.scroll-content > * {
max-width: 600px;
margin: 0 auto;
width: 100%;
}
.core-login-site-logo {
margin-top: 20%;
}
&.hidden {
margin: 0;
}
}
.core-login-entered-site {
background-color: $gray-lighter;
ion-thumbnail {
box-shadow: 0 0 4px #ddd;
}
}
.core-login-default-icon {
filter: grayscale(100%);
}
}

View File

@ -13,36 +13,26 @@
// limitations under the License.
import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, ModalController, NavParams } from 'ionic-angular';
import { IonicPage, NavController, ModalController, AlertController, NavParams } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider, CoreSiteCheckResponse } from '@providers/sites';
import { CoreSitesProvider, CoreSiteCheckResponse, CoreLoginSiteInfo } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreConfigConstants } from '../../../../configconstants';
import { CoreLoginHelperProvider } from '../../providers/helper';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FormBuilder, FormGroup, ValidatorFn, AbstractControl } from '@angular/forms';
import { CoreUrl } from '@singletons/url';
import { TranslateService } from '@ngx-translate/core';
/**
* Data about an error when connecting to a site.
* Extended data for UI implementation.
*/
type CoreLoginSiteError = {
/**
* The error message that ocurred.
*/
message: string;
/**
* URL the user entered.
*/
url?: string;
/**
* URL the user entered with protocol added if needed.
*/
fullUrl?: string;
type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & {
fromWS?: boolean; // If the site came from the WS call.
noProtocolUrl?: string; // Url wihtout protocol.
country?: string; // Based on countrycode.
};
/**
@ -58,12 +48,16 @@ export class CoreLoginSitePage {
@ViewChild('siteFormEl') formElement: ElementRef;
siteForm: FormGroup;
fixedSites: any[];
filteredSites: any[];
fixedSites: CoreLoginSiteInfo[];
filteredSites: CoreLoginSiteInfo[];
fixedDisplay = 'buttons';
showKeyboard = false;
filter = '';
error: CoreLoginSiteError;
sites: CoreLoginSiteInfoExtended[] = [];
hasSites = false;
loadingSites = false;
onlyWrittenSite = false;
searchFnc: Function;
constructor(navParams: NavParams,
protected navCtrl: NavController,
@ -72,10 +66,12 @@ export class CoreLoginSitePage {
protected sitesProvider: CoreSitesProvider,
protected loginHelper: CoreLoginHelperProvider,
protected modalCtrl: ModalController,
protected alertCtrl: AlertController,
protected urlUtils: CoreUrlUtilsProvider,
protected domUtils: CoreDomUtilsProvider,
protected eventsProvider: CoreEventsProvider,
protected translate: TranslateService,
protected urlUtils: CoreUrlUtilsProvider) {
protected utils: CoreUtilsProvider) {
this.showKeyboard = !!navParams.get('showKeyboard');
@ -94,8 +90,44 @@ export class CoreLoginSitePage {
}
this.siteForm = fb.group({
siteUrl: [url, Validators.required]
siteUrl: [url, this.moodleUrlValidator()]
});
this.searchFnc = this.utils.debounce(async (search: string, isValid: boolean = false) => {
search = search.trim();
if (search.length >= 3) {
this.onlyWrittenSite = false;
// Update the sites list.
this.sites = await this.sitesProvider.findSites(search);
// UI tweaks.
this.sites.forEach((site) => {
site.noProtocolUrl = CoreUrl.removeProtocol(site.url);
site.fromWS = true;
site.country = this.utils.getCountryName(site.countrycode);
});
// If it's a valid URL, add it.
if (isValid) {
this.onlyWrittenSite = !!this.sites.length;
this.sites.unshift({
url: search,
fromWS: false,
name: this.translate.instant('core.login.yourenteredsite'),
noProtocolUrl: CoreUrl.removeProtocol(search),
});
}
this.hasSites = !!this.sites.length;
} else {
// Not reseting the array to allow animation to be displayed.
this.hasSites = false;
}
this.loadingSites = false;
}, 1000);
}
/**
@ -103,8 +135,9 @@ export class CoreLoginSitePage {
*
* @param e Event.
* @param url The URL to connect to.
* @param foundSite The site clicked, if any, from the found sites list.
*/
connect(e: Event, url: string): void {
connect(e: Event, url: string, foundSite?: CoreLoginSiteInfoExtended): void {
e.preventDefault();
e.stopPropagation();
@ -130,8 +163,6 @@ export class CoreLoginSitePage {
return;
}
this.hideLoginIssue();
const modal = this.domUtils.showModalLoading(),
siteData = this.sitesProvider.getDemoSiteData(url);
@ -174,7 +205,7 @@ export class CoreLoginSitePage {
return Promise.reject(error);
})
.then((result) => this.login(result))
.then((result) => this.login(result, foundSite))
.catch((error) => this.showLoginIssue(url, error))
.finally(() => modal.dismiss());
}
@ -204,13 +235,6 @@ export class CoreLoginSitePage {
modal.present();
}
/**
* Hide the login error.
*/
protected hideLoginIssue(): void {
this.error = null;
}
/**
* Show an error that aims people to solve the issue.
*
@ -218,13 +242,60 @@ export class CoreLoginSitePage {
* @param error Error to display.
*/
protected showLoginIssue(url: string, error: any): void {
this.error = {
url: url,
message: this.domUtils.getErrorMessage(error),
};
error = this.domUtils.getErrorMessage(error);
if (error == this.translate.instant('core.cannotconnecttrouble')) {
const found = this.sites.find((site) => site.fromWS && site.url == url);
if (!found) {
error += ' ' + this.translate.instant('core.cannotconnectverify');
}
}
let message = '<p>' + error + '</p>';
if (url) {
this.error.fullUrl = this.urlUtils.isAbsoluteURL(url) ? url : 'https://' + url;
const fullUrl = this.urlUtils.isAbsoluteURL(url) ? url : 'https://' + url;
message += '<p padding><a href="' + fullUrl + '" core-link>' + url + '</a></p>';
}
const buttons = [
{
text: this.translate.instant('core.needhelp'),
handler: (): void => {
this.showHelp();
}
},
{
text: this.translate.instant('core.tryagain'),
role: 'cancel'
}
];
this.domUtils.showAlertWithButtons(this.translate.instant('core.cannotconnect'), message, buttons);
}
/**
* Find a site on the backend.
*
* @param e Event.
* @param search Text to search.
*/
searchSite(e: Event, search: string): void {
this.loadingSites = true;
this.searchFnc(search.trim(), this.siteForm.valid);
}
/**
* Get the demo data for a certain "name" if it is a demo site.
*
* @param name Name of the site to check.
* @return Site data if it's a demo site, undefined otherwise.
*/
getDemoSiteData(name: string): any {
const demoSites = CoreConfigConstants.demo_sites;
if (typeof demoSites != 'undefined' && typeof demoSites[name] != 'undefined') {
return demoSites[name];
}
}
@ -232,10 +303,11 @@ export class CoreLoginSitePage {
* Process login to a site.
*
* @param response Response obtained from the site check request.
* @param foundSite The site clicked, if any, from the found sites list.
*
* @return Promise resolved after logging in.
*/
protected async login(response: CoreSiteCheckResponse): Promise<void> {
protected async login(response: CoreSiteCheckResponse, foundSite?: CoreLoginSiteInfoExtended): Promise<void> {
return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => {
this.domUtils.triggerFormSubmittedEvent(this.formElement, true);
@ -249,11 +321,39 @@ export class CoreLoginSitePage {
this.loginHelper.confirmAndOpenBrowserForSSOLogin(
response.siteUrl, response.code, response.service, response.config && response.config.launchurl);
} else {
this.navCtrl.push('CoreLoginCredentialsPage', { siteUrl: response.siteUrl, siteConfig: response.config });
const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config };
if (foundSite) {
pageParams['siteName'] = foundSite.name;
pageParams['logoUrl'] = foundSite.imageurl;
}
this.navCtrl.push('CoreLoginCredentialsPage', pageParams);
}
}).catch(() => {
// Ignore errors.
});
}
/**
* Validate Url.
*
* @return {ValidatorFn} Validation results.
*/
protected moodleUrlValidator(): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
const value = control.value.trim();
let valid = value.length >= 3 && CoreUrl.isValidMoodleUrl(value);
if (!valid) {
const demo = !!this.getDemoSiteData(value);
if (demo) {
valid = true;
}
}
return valid ? null : {siteUrl: {value: control.value}};
};
}
}

View File

@ -12,7 +12,9 @@
"back": "Back",
"browser": "Browser",
"cancel": "Cancel",
"cannotconnect": "<strong>Cannot connect</strong>: Verify that you have correctly typed your site address.",
"cannotconnect": "Cannot connect",
"cannotconnecttrouble": "We're having trouble connecting to your site.",
"cannotconnectverify": "<strong>Please check the address is correct.</strong>",
"cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.",
"captureaudio": "Record audio",
"capturedimage": "Taken picture.",

View File

@ -165,6 +165,41 @@ export interface CoreSiteSchema {
migrate?(db: SQLiteDB, oldVersion: number, siteId: string): Promise<any> | void;
}
/**
* Data about sites to be listed.
*/
export interface CoreLoginSiteInfo {
/**
* Site name.
*/
name: string;
/**
* Site alias.
*/
alias?: string;
/**
* URL of the site.
*/
url: string;
/**
* Image URL of the site.
*/
imageurl?: string;
/**
* City of the site.
*/
city?: string;
/**
* Countrycode of the site.
*/
countrycode?: string;
}
/**
* Registered site schema.
*/
@ -367,10 +402,17 @@ export class CoreSitesProvider {
]
};
constructor(logger: CoreLoggerProvider, private http: HttpClient, private sitesFactory: CoreSitesFactoryProvider,
private appProvider: CoreAppProvider, private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider,
private eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider,
private utils: CoreUtilsProvider, private injector: Injector, private wsProvider: CoreWSProvider,
constructor(logger: CoreLoggerProvider,
protected http: HttpClient,
protected sitesFactory: CoreSitesFactoryProvider,
protected appProvider: CoreAppProvider,
protected translate: TranslateService,
protected urlUtils: CoreUrlUtilsProvider,
protected eventsProvider: CoreEventsProvider,
protected textUtils: CoreTextUtilsProvider,
protected utils: CoreUtilsProvider,
protected injector: Injector,
protected wsProvider: CoreWSProvider,
protected domUtils: CoreDomUtilsProvider) {
this.logger = logger.getInstance('CoreSitesProvider');
@ -431,7 +473,7 @@ export class CoreSitesProvider {
} else if (this.textUtils.getErrorMessageFromError(secondError)) {
return Promise.reject(secondError);
} else {
return this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION});
return this.translate.instant('core.cannotconnecttrouble');
}
});
});
@ -523,8 +565,7 @@ export class CoreSitesProvider {
error.error = this.translate.instant('core.login.sitehasredirect');
} else {
// We can't be sure if there is a redirect or not. Display cannot connect error.
error.error = this.translate.instant('core.cannotconnect',
{$a: CoreSite.MINIMUM_MOODLE_VERSION});
error.error = this.translate.instant('core.cannotconnecttrouble');
}
return Promise.reject(error);
@ -569,7 +610,7 @@ export class CoreSitesProvider {
return this.http.post(siteUrl + '/login/token.php', {}).timeout(this.wsProvider.getRequestTimeout()).toPromise()
.catch(() => {
// Default error messages are kinda bad, return our own message.
return Promise.reject({error: this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION})});
return Promise.reject({error: this.translate.instant('core.cannotconnecttrouble')});
}).then((data: any) => {
if (data === null) {
@ -616,7 +657,7 @@ export class CoreSitesProvider {
return promise.then((data: any): any => {
if (typeof data == 'undefined') {
return Promise.reject(this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION}));
return Promise.reject(this.translate.instant('core.cannotconnecttrouble'));
} else {
if (typeof data.token != 'undefined') {
return { token: data.token, siteUrl: siteUrl, privateToken: data.privatetoken };
@ -648,7 +689,7 @@ export class CoreSitesProvider {
}
}
}, () => {
return Promise.reject(this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION}));
return Promise.reject(this.translate.instant('core.cannotconnecttrouble'));
});
}
@ -1931,6 +1972,16 @@ export class CoreSitesProvider {
return {};
}
}
/**
* Returns site info found on the backend.
*
* @param search Searched text.
* @return Site info list.
*/
async findSites(search: string): Promise<CoreLoginSiteInfo[]> {
return [];
}
}
export class CoreSites extends makeSingleton(CoreSitesProvider) {}

View File

@ -15,7 +15,7 @@
import { Injectable, SimpleChange, ElementRef } from '@angular/core';
import {
LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform, Content, PopoverController,
ModalController,
ModalController, AlertButton
} from 'ionic-angular';
import { DomSanitizer } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
@ -1138,65 +1138,76 @@ export class CoreDomUtilsProvider {
* @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
* @return Promise resolved with the alert modal.
*/
showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise<CoreAlert> {
async showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise<CoreAlert> {
const buttons = [buttonText || this.translate.instant('core.ok')];
return this.showAlertWithButtons(title, message, buttons, autocloseTime);
}
/**
* Show an alert modal with some buttons.
*
* @param title Title to show.
* @param message Message to show.
* @param buttons Buttons objects or texts.
* @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
* @return Promise resolved with the alert modal.
*/
async showAlertWithButtons(title: string, message: string, buttons: (string | AlertButton)[], autocloseTime?: number):
Promise<CoreAlert> {
const hasHTMLTags = this.textUtils.hasHTMLTags(message);
let promise;
if (hasHTMLTags) {
// Format the text.
promise = this.textUtils.formatText(message);
} else {
promise = Promise.resolve(message);
message = await this.textUtils.formatText(message);
}
return promise.then((message) => {
const alertId = <string> Md5.hashAsciiStr((title || '') + '#' + (message || ''));
const alertId = <string> Md5.hashAsciiStr((title || '') + '#' + (message || ''));
if (this.displayedAlerts[alertId]) {
// There's already an alert with the same message and title. Return it.
return this.displayedAlerts[alertId];
}
if (this.displayedAlerts[alertId]) {
// There's already an alert with the same message and title. Return it.
return this.displayedAlerts[alertId];
}
const alert: CoreAlert = <any> this.alertCtrl.create({
title: title,
message: message,
buttons: [buttonText || this.translate.instant('core.ok')]
});
alert.present().then(() => {
if (hasHTMLTags) {
// Treat all anchors so they don't override the app.
const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message');
this.treatAnchors(alertMessageEl);
}
});
// Store the alert and remove it when dismissed.
this.displayedAlerts[alertId] = alert;
// Define the observables to extend the Alert class. This will allow several callbacks instead of just one.
alert.didDismiss = new Subject();
alert.willDismiss = new Subject();
// Set the callbacks to trigger an observable event.
alert.onDidDismiss((data: any, role: string) => {
delete this.displayedAlerts[alertId];
alert.didDismiss.next({data: data, role: role});
});
alert.onWillDismiss((data: any, role: string) => {
alert.willDismiss.next({data: data, role: role});
});
if (autocloseTime > 0) {
setTimeout(() => {
alert.dismiss();
}, autocloseTime);
}
return alert;
const alert: CoreAlert = <any> this.alertCtrl.create({
title: title,
message: message,
buttons: buttons,
});
alert.present().then(() => {
if (hasHTMLTags) {
// Treat all anchors so they don't override the app.
const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message');
this.treatAnchors(alertMessageEl);
}
});
// Store the alert and remove it when dismissed.
this.displayedAlerts[alertId] = alert;
// Define the observables to extend the Alert class. This will allow several callbacks instead of just one.
alert.didDismiss = new Subject();
alert.willDismiss = new Subject();
// Set the callbacks to trigger an observable event.
alert.onDidDismiss((data: any, role: string) => {
delete this.displayedAlerts[alertId];
alert.didDismiss.next({data: data, role: role});
});
alert.onWillDismiss((data: any, role: string) => {
alert.willDismiss.next({data: data, role: role});
});
if (autocloseTime > 0) {
setTimeout(() => {
alert.dismiss();
}, autocloseTime);
}
return alert;
}
/**

View File

@ -225,7 +225,7 @@ export class CoreUrlUtilsProvider {
url = 'https://' + url;
}
// http allways in lowercase.
// http always in lowercase.
url = url.replace(/^http/i, 'http');
url = url.replace(/^https/i, 'https');

View File

@ -119,4 +119,36 @@ export class CoreUrl {
return urlParts && urlParts.domain ? urlParts.domain : null;
}
/**
* Returns the pattern to check if the URL is a valid Moodle Url.
*
* @return {RegExp} Desired RegExp.
*/
static getValidMoodleUrlPattern(): RegExp {
// Regular expression based on RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B.
// Improved to not admit spaces.
return new RegExp(/^(([^:/?# ]+):)?(\/\/([^/?# ]*))?([^?# ]*)(\?([^#]*))?(#(.*))?$/);
}
/**
* Check if the given url is valid for the app to connect.
*
* @param {string} url Url to check.
* @return {boolean} True if valid, false otherwise.
*/
static isValidMoodleUrl(url: string): boolean {
const patt = CoreUrl.getValidMoodleUrlPattern();
return patt.test(url.trim());
}
/**
* Removes protocol from the url.
*
* @param url Site url.
* @return Url without protocol.
*/
static removeProtocol(url: string): string {
return url.replace(/^[a-zA-Z]+:\/\//i, '');
}
}

View File

@ -6,16 +6,6 @@ $core-dark-item-bg-color: $gray-darker !default;
$core-dark-item-divider-bg-color: $gray-dark !default;
$core-dark-background-color: $black !default;
// Login.
$core-dark-login-page-background-color: radial-gradient(white, $gray-dark) !default;
$core-dark-login-box-background-color: $black !default;
$core-dark-login-box-background-border: $core-login-box-background-border !default;
$core-dark-login-box-text-color: $core-dark-text-color !default;
$core-dark-login-item-inner-background-color: $core-dark-login-box-background-color !default;
$core-dark-login-item-background-color: $core-dark-login-box-background-color !default;
$core-dark-login-button-outline: $core-login-button-outline !default;
$core-dark-login-loading-color: $core-dark-text-color !default;
ion-app.app-root {
@include darkmode() {
ion-action-sheet .action-sheet-container .action-sheet-group .action-sheet-button {

View File

@ -184,16 +184,6 @@ $core-button-outline-background-color: $white !default;
$core-network-message-height: 16px !default;
// Login.
$core-login-page-background-color: radial-gradient(white, $gray-light) !default;
$core-login-box-background-color: $white !default;
$core-login-box-background-border: $gray !default;
$core-login-box-text-color: $text-color !default;
$core-login-button-outline: false !default;
$core-login-loading-color: false !default;
$core-login-item-inner-background-color: $white !default;
$core-login-item-background-color: $white !default;
$core-action-sheet-color: $core-color !default;
$core-action-sheet-cancel-color: $danger !default;
$core-dark-action-sheet-cancel-color: $danger-dark !default;