MOBILE-2253 core: Implement components and directives needed by login
parent
1019e591ec
commit
28d6203784
|
@ -14,17 +14,30 @@
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { IonicModule } from 'ionic-angular';
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreDirectivesModule } from '../directives/directives.module';
|
||||||
import { CoreLoadingComponent } from './loading/loading';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreLoadingComponent
|
CoreLoadingComponent,
|
||||||
|
CoreMarkRequiredComponent,
|
||||||
|
CoreInputErrorsComponent,
|
||||||
|
CoreShowPasswordComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
IonicModule
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreDirectivesModule
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CoreLoadingComponent
|
CoreLoadingComponent,
|
||||||
|
CoreMarkRequiredComponent,
|
||||||
|
CoreInputErrorsComponent,
|
||||||
|
CoreShowPasswordComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
export class CoreComponentsModule {}
|
||||||
|
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
core-input-errors {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.core-input-error-container {
|
||||||
|
.core-input-error {
|
||||||
|
padding: 4px;
|
||||||
|
color: red;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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. -->
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ import { CoreDomUtilsProvider } from '../providers/utils/dom';
|
||||||
selector: '[core-auto-focus]'
|
selector: '[core-auto-focus]'
|
||||||
})
|
})
|
||||||
export class CoreAutoFocusDirective implements AfterViewInit {
|
export class CoreAutoFocusDirective implements AfterViewInit {
|
||||||
@Input('core-auto-focus') coreAutoFocus: boolean = true;
|
@Input('core-auto-focus') coreAutoFocus: boolean|string = true;
|
||||||
|
|
||||||
protected element: HTMLElement;
|
protected element: HTMLElement;
|
||||||
|
|
||||||
|
@ -36,8 +36,14 @@ export class CoreAutoFocusDirective implements AfterViewInit {
|
||||||
* Function after the view is initialized.
|
* Function after the view is initialized.
|
||||||
*/
|
*/
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.coreAutoFocus = typeof this.coreAutoFocus != 'boolean' ? true : this.coreAutoFocus;
|
let autoFocus;
|
||||||
if (this.coreAutoFocus) {
|
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.
|
// If it's a ion-input or ion-textarea, search the right input to use.
|
||||||
let element = this.element;
|
let element = this.element;
|
||||||
if (this.element.tagName == 'ION-INPUT') {
|
if (this.element.tagName == 'ION-INPUT') {
|
||||||
|
|
|
@ -17,12 +17,14 @@ import { CoreAutoFocusDirective } from './auto-focus';
|
||||||
import { CoreExternalContentDirective } from './external-content';
|
import { CoreExternalContentDirective } from './external-content';
|
||||||
import { CoreFormatTextDirective } from './format-text';
|
import { CoreFormatTextDirective } from './format-text';
|
||||||
import { CoreLinkDirective } from './link';
|
import { CoreLinkDirective } from './link';
|
||||||
|
import { CoreKeepKeyboardDirective } from './keep-keyboard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreAutoFocusDirective,
|
CoreAutoFocusDirective,
|
||||||
CoreExternalContentDirective,
|
CoreExternalContentDirective,
|
||||||
CoreFormatTextDirective,
|
CoreFormatTextDirective,
|
||||||
|
CoreKeepKeyboardDirective,
|
||||||
CoreLinkDirective
|
CoreLinkDirective
|
||||||
],
|
],
|
||||||
imports: [],
|
imports: [],
|
||||||
|
@ -30,6 +32,7 @@ import { CoreLinkDirective } from './link';
|
||||||
CoreAutoFocusDirective,
|
CoreAutoFocusDirective,
|
||||||
CoreExternalContentDirective,
|
CoreExternalContentDirective,
|
||||||
CoreFormatTextDirective,
|
CoreFormatTextDirective,
|
||||||
|
CoreKeepKeyboardDirective,
|
||||||
CoreLinkDirective
|
CoreLinkDirective
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue