MOBILE-2253 core: Implement components and directives needed by login

main
Dani Palou 2017-11-29 14:36:29 +01:00
parent 1019e591ec
commit 28d6203784
13 changed files with 526 additions and 6 deletions

View File

@ -14,17 +14,30 @@
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '../directives/directives.module';
import { CoreLoadingComponent } from './loading/loading';
import { CoreMarkRequiredComponent } from './mark-required/mark-required';
import { CoreInputErrorsComponent } from './input-errors/input-errors';
import { CoreShowPasswordComponent } from './show-password/show-password';
@NgModule({
declarations: [
CoreLoadingComponent
CoreLoadingComponent,
CoreMarkRequiredComponent,
CoreInputErrorsComponent,
CoreShowPasswordComponent
],
imports: [
IonicModule
IonicModule,
TranslateModule.forChild(),
CoreDirectivesModule
],
exports: [
CoreLoadingComponent
CoreLoadingComponent,
CoreMarkRequiredComponent,
CoreInputErrorsComponent,
CoreShowPasswordComponent
]
})
export class CoreComponentsModule {}

View File

@ -0,0 +1,5 @@
<div class="core-input-error-container" *ngIf="formControl.dirty && !formControl.valid" role="alert">
<div *ngFor="let error of errorKeys">
<div *ngIf="formControl.hasError(error)" class="core-input-error">{{errorMessages[error]}}</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
core-input-errors {
width: 100%;
.core-input-error-container {
.core-input-error {
padding: 4px;
color: red;
font-size: 12px;
}
}
}

View File

@ -0,0 +1,96 @@
// (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, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
/**
* Component to show errors if an input isn't valid.
*
* @description
* The purpose of this component is to make easier and consistent the validation of forms.
*
* It should be applied next to the input element (ion-input, ion-select, ...). In case of ion-checkbox, it should be in another
* item, placing it in the same item as the checkbox will cause problems.
*
* Please notice that the inputs need to have a FormControl to make it work. That FormControl needs to be passed to this component.
*
* If this component is placed in the same ion-item as a ion-label or ion-input, then it should have the attribute "item-content",
* otherwise Ionic will remove it.
*
* Example usage:
*
* <ion-item text-wrap>
* <ion-label stacked core-mark-required="true">{{ 'mm.login.username' | translate }}</ion-label>
* <ion-input type="text" name="username" formControlName="username"></ion-input>
* <core-input-errors item-content [control]="myForm.controls.username" [errorMessages]="usernameErrors"></core-input-errors>
* </ion-item>
*/
@Component({
selector: 'core-input-errors',
templateUrl: 'input-errors.html'
})
export class CoreInputErrorsComponent implements OnInit {
@Input('control') formControl: FormControl;
@Input() errorMessages?: any;
errorKeys: any[];
constructor(private translate: TranslateService) {}
/**
* Component is being initialized.
*/
ngOnInit() {
this.initErrorMessages();
this.errorKeys = Object.keys(this.errorMessages);
}
/**
* Initialize some common errors if they aren't set.
*/
protected initErrorMessages() {
this.errorMessages = this.errorMessages || {};
this.errorMessages.required = this.errorMessages.required || this.translate.instant('mm.core.required');
this.errorMessages.email = this.errorMessages.email || this.translate.instant('mm.login.invalidemail');
this.errorMessages.date = this.errorMessages.date || this.translate.instant('mm.login.invaliddate');
this.errorMessages.datetime = this.errorMessages.datetime || this.translate.instant('mm.login.invaliddate');
this.errorMessages.datetimelocal = this.errorMessages.datetimelocal || this.translate.instant('mm.login.invaliddate');
this.errorMessages.time = this.errorMessages.time || this.translate.instant('mm.login.invalidtime');
this.errorMessages.url = this.errorMessages.url || this.translate.instant('mm.login.invalidurl');
// @todo: Check how to handle min/max errors once we have a test case to use. Also, review previous errors.
// ['min', 'max'].forEach((type) => {
// // Initialize min/max errors if needed.
// if (!this.errorMessages[type]) {
// if (input && typeof input[type] != 'undefined' && input[type] !== '') {
// var value = input[type];
// if (input.type == 'date' || input.type == 'datetime' || input.type == 'datetime-local') {
// var date = moment(value);
// if (date.isValid()) {
// value = moment(value).format($translate.instant('mm.core.dfdaymonthyear'));
// }
// }
// scope.errorMessages[type] = $translate.instant('mm.login.invalidvalue' + type, {$a: value});
// } else {
// scope.errorMessages[type] = $translate.instant('mm.login.profileinvaliddata');
// }
// }
// });
}
}

View File

@ -0,0 +1,2 @@
<ng-content></ng-content>
<ion-icon *ngIf="coreMarkRequired && coreMarkRequired !== 'false'" class="core-input-required-asterisk" name="medical" md="ios-medical" [title]="requiredLabel"></ion-icon> <!-- Use iOS icon because it's more narrow, so it looks better since it's small. -->

View File

@ -0,0 +1,9 @@
*[core-mark-required] {
.core-input-required-asterisk, .icon.core-input-required-asterisk {
color: red !important;
font-size: 8px;
padding-left: 4px;
line-height: 100%;
vertical-align: top;
}
}

View File

@ -0,0 +1,61 @@
// (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, Input, AfterViewInit, ElementRef } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from '../../providers/utils/text';
/**
* Directive to add a red asterisk for required input fields.
*
* @description
* For forms with required and not required fields, it is recommended to use this directive to mark the required ones.
*
* This directive should be applied in the label. Example:
*
* <ion-label core-mark-required="{{field.required}}">{{ 'mm.login.username' | translate }}</ion-label>
*/
@Component({
selector: '[core-mark-required]',
templateUrl: 'mark-required.html'
})
export class CoreMarkRequiredComponent implements AfterViewInit {
@Input('core-mark-required') coreMarkRequired: boolean|string = true;
protected element: HTMLElement;
requiredLabel: string;
constructor(element: ElementRef, private translate: TranslateService, private textUtils: CoreTextUtilsProvider) {
this.element = element.nativeElement;
this.requiredLabel = this.translate.instant('mm.core.required');
}
/**
* Called after the view is initialized.
*/
ngAfterViewInit() : void {
if (this.coreMarkRequired) {
// Add the "required" to the aria-label.
const ariaLabel = this.element.getAttribute('aria-label') || this.textUtils.cleanTags(this.element.innerHTML, true);
if (ariaLabel) {
this.element.setAttribute('aria-label', ariaLabel + ' ' + this.requiredLabel);
}
} else {
// Remove the "required" from the aria-label.
const ariaLabel = this.element.getAttribute('aria-label');
if (ariaLabel) {
this.element.setAttribute('aria-label', ariaLabel.replace(' ' + this.requiredLabel, ''));
}
}
}
}

View File

@ -0,0 +1,4 @@
<ng-content></ng-content>
<a ion-button icon-only clear [attr.aria-label]="label | translate" (click)="toggle()" [core-keep-keyboard]="selector" [inButton]="true">
<ion-icon [name]="iconName"></ion-icon>
</a>

View File

@ -0,0 +1,24 @@
core-show-password {
padding: 0px;
width: 100%;
position: relative;
ion-input {
padding-right: 47 + $content-padding;
}
.button[icon-only] {
padding: 0 ($content-padding / 2);
position: absolute;
top: $content-padding / 2;
right: 0;
margin-top: 0;
margin-bottom: 0;
}
}
.item-label-stacked core-show-password {
.button[icon-only] {
top: 0;
}
}

View File

@ -0,0 +1,106 @@
// (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, OnInit, AfterViewInit, Input, ElementRef } from '@angular/core';
/**
* Component to allow showing and hiding a password. The affected input MUST have a name to identify it.
*
* @description
* This directive needs to surround the input with the password.
*
* You need to supply the name of the input.
*
* Example:
*
* <core-show-password item-content [name]="'password'">
* <ion-input type="password" name="password"></ion-input>
* </core-show-password>
*/
@Component({
selector: 'core-show-password',
templateUrl: 'show-password.html'
})
export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
@Input() name: string; // Name of the input affected.
@Input() initialShown?: boolean|string; // Whether the password should be shown at start.
shown: boolean; // Whether the password is shown.
label: string; // Label for the button to show/hide.
iconName: string; // Name of the icon of the button to show/hide.
selector: string = ''; // Selector to identify the input.
protected input: HTMLInputElement; // Input affected.
protected element: HTMLElement; // Current element.
constructor(element: ElementRef) {
this.element = element.nativeElement;
}
/**
* Component being initialized.
*/
ngOnInit() {
this.shown = this.initialShown && this.initialShown !== 'false';
this.selector = 'input[name="' + this.name + '"]';
this.setData();
}
/**
* View has been initialized.
*/
ngAfterViewInit() {
this.searchInput();
}
/**
* Search the input to show/hide.
*/
protected searchInput() {
// Search the input.
this.input = <HTMLInputElement> this.element.querySelector(this.selector);
if (this.input) {
// Input found. Set the right type.
this.input.type = this.shown ? 'text' : 'password';
// By default, don't autocapitalize and autocorrect.
if (!this.input.getAttribute('autocorrect')) {
this.input.setAttribute('autocorrect', 'off');
}
if (!this.input.getAttribute('autocapitalize')) {
this.input.setAttribute('autocapitalize', 'none');
}
}
}
/**
* Set label, icon name and input type.
*/
protected setData() {
this.label = this.shown ? 'mm.core.hide' : 'mm.core.show';
this.iconName = this.shown ? 'eye-off' : 'eye';
if (this.input) {
this.input.type = this.shown ? 'text' : 'password';
}
}
/**
* Toggle show/hide password.
*/
toggle() : void {
this.shown = !this.shown;
this.setData();
}
}

View File

@ -24,7 +24,7 @@ import { CoreDomUtilsProvider } from '../providers/utils/dom';
selector: '[core-auto-focus]'
})
export class CoreAutoFocusDirective implements AfterViewInit {
@Input('core-auto-focus') coreAutoFocus: boolean = true;
@Input('core-auto-focus') coreAutoFocus: boolean|string = true;
protected element: HTMLElement;
@ -36,8 +36,14 @@ export class CoreAutoFocusDirective implements AfterViewInit {
* Function after the view is initialized.
*/
ngAfterViewInit() {
this.coreAutoFocus = typeof this.coreAutoFocus != 'boolean' ? true : this.coreAutoFocus;
if (this.coreAutoFocus) {
let autoFocus;
if (typeof this.coreAutoFocus == 'string') {
autoFocus = this.coreAutoFocus && this.coreAutoFocus !== 'false';
} else {
autoFocus = !!this.coreAutoFocus;
}
if (autoFocus) {
// If it's a ion-input or ion-textarea, search the right input to use.
let element = this.element;
if (this.element.tagName == 'ION-INPUT') {

View File

@ -17,12 +17,14 @@ import { CoreAutoFocusDirective } from './auto-focus';
import { CoreExternalContentDirective } from './external-content';
import { CoreFormatTextDirective } from './format-text';
import { CoreLinkDirective } from './link';
import { CoreKeepKeyboardDirective } from './keep-keyboard';
@NgModule({
declarations: [
CoreAutoFocusDirective,
CoreExternalContentDirective,
CoreFormatTextDirective,
CoreKeepKeyboardDirective,
CoreLinkDirective
],
imports: [],
@ -30,6 +32,7 @@ import { CoreLinkDirective } from './link';
CoreAutoFocusDirective,
CoreExternalContentDirective,
CoreFormatTextDirective,
CoreKeepKeyboardDirective,
CoreLinkDirective
]
})

View File

@ -0,0 +1,180 @@
// (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 { Directive, AfterViewInit, Input, ElementRef, OnDestroy } from '@angular/core';
import { CoreDomUtilsProvider } from '../providers/utils/dom';
/**
* Directive to keep the keyboard open when clicking a certain element (usually a button).
*
* @description
*
* This directive needs to be applied to an input or textarea. The value of the directive needs to be a selector
* to identify the element to listen for clicks (usually a button).
*
* When that element is clicked, the input that has this directive will keep the focus if it has it already and the keyboard
* won't be closed.
*
* Example usage:
*
* <textarea [mm-keep-keyboard]="'#mma-messages-send-message-button'"></textarea>
* <button id="mma-messages-send-message-button">Send</button>
*
* Alternatively, this directive can be applied to the button. The value of the directive needs to be a selector to identify
* the input element. In this case, you need to set [inButton]="true".
*
* Example usage:
*
* <textarea id="send-message-input"></textarea>
* <button [mm-keep-keyboard]="'#send-message-input'" [inButton]="true">Send</button>
*/
@Directive({
selector: '[core-keep-keyboard]'
})
export class CoreKeepKeyboardDirective implements AfterViewInit, OnDestroy {
@Input('core-keep-keyboard') selector: string; // Selector to identify the button or input.
@Input() inButton?: boolean|string; // Whether this directive is applied to the button (true) or to the input (false).
protected element: HTMLElement; // Current element.
protected button: HTMLElement; // Button element.
protected input: HTMLElement; // Input element.
protected lastFocusOut = 0; // Last time the input was focused out.
protected clickListener : any; // Listener for clicks in the button.
protected focusOutListener : any; // Listener for focusout in the input.
protected focusAgainListener : any; // Another listener for focusout, with the purpose to focus again.
protected stopFocusAgainTimeout: any; // Timeout to stop focus again listener.
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider) {
this.element = element.nativeElement;
}
/**
* View has been initialized.
*/
ngAfterViewInit() {
// Use a setTimeout because, if this directive is applied to a button, then the ion-input that it affects
// maybe it hasn't been treated yet.
setTimeout(() => {
let inButton = this.inButton && this.inButton !== 'false',
candidateEls,
selectedEl;
if (typeof this.selector != 'string' || !this.selector) {
// Not a valid selector, stop.
return;
}
// Get the selected element. Get the last one found.
candidateEls = document.querySelectorAll(this.selector);
selectedEl = candidateEls[candidateEls.length - 1];
if (!selectedEl) {
// Element not found.
return;
}
if (inButton) {
// The directive is applied to the button.
this.button = this.element;
this.input = selectedEl;
} else {
// The directive is applied to the input.
this.button = selectedEl;
if (this.element.tagName == 'ION-INPUT') {
// Search the inner input.
this.input = this.element.querySelector('input');
} else if (this.element.tagName == 'ION-TEXTAREA') {
// Search the inner textarea.
this.input = this.element.querySelector('textarea');
} else {
this.input = this.element;
}
if (!this.input) {
// Input not found, stop.
return;
}
}
// Listen for focusout event. This is to be able to check if previous focus was on this element.
this.focusOutListener = this.focusOut.bind(this);
this.input.addEventListener('focusout', this.focusOutListener);
// Listen for clicks in the button.
this.clickListener = this.buttonClicked.bind(this);
this.button.addEventListener('click', this.clickListener);
});
}
/**
* Component destroyed.
*/
ngOnDestroy() {
if (this.button && this.clickListener) {
this.button.removeEventListener('click', this.clickListener);
}
if (this.input && this.focusOutListener) {
this.input.removeEventListener('focusout', this.focusOutListener);
}
}
/**
* The button we're interested in was clicked.
*/
protected buttonClicked() : void {
if (document.activeElement == this.input) {
// Directive's element is focused at the time the button is clicked. Listen for focusout to focus it again.
this.focusAgainListener = this.focusElementAgain.bind(this);
this.input.addEventListener('focusout', this.focusAgainListener);
// Focus it after a timeout just in case the focusout event isn't triggered.
// @todo: This doesn't seem to be needed in iOS. We should test it in Android.
// setTimeout(() => {
// if (this.focusAgainListener) {
// this.focusElementAgain();
// }
// }, 1000);
} else if (document.activeElement == this.button && Date.now() - this.lastFocusOut < 200) {
// Last focused element was the directive's element, focus it again.
setTimeout(this.focusElementAgain.bind(this), 0);
}
}
/**
* If keyboard is open, focus the input again and stop listening focusout to focus again if needed.
*/
protected focusElementAgain() : void {
this.domUtils.focusElement(this.input);
if (this.focusAgainListener) {
// Sometimes we can receive more than 1 focus out event. If we spend 1 second without receiving any,
// stop listening for them.
let listener = this.focusAgainListener; // Store it in a local variable, in case it changes.
clearTimeout(this.stopFocusAgainTimeout);
this.stopFocusAgainTimeout = setTimeout(() => {
this.input.removeEventListener('focusout', listener);
if (listener == this.focusAgainListener) {
delete this.focusAgainListener;
}
}, 1000);
}
}
/**
* Input was focused out, save the time it was done.
*/
protected focusOut() : void {
this.lastFocusOut = Date.now();
}
}