\ No newline at end of file
diff --git a/src/components/input-errors/input-errors.scss b/src/components/input-errors/input-errors.scss
new file mode 100644
index 000000000..c432be9ab
--- /dev/null
+++ b/src/components/input-errors/input-errors.scss
@@ -0,0 +1,11 @@
+core-input-errors {
+ width: 100%;
+
+ .core-input-error-container {
+ .core-input-error {
+ padding: 4px;
+ color: red;
+ font-size: 12px;
+ }
+ }
+}
diff --git a/src/components/input-errors/input-errors.ts b/src/components/input-errors/input-errors.ts
new file mode 100644
index 000000000..1e40cbee0
--- /dev/null
+++ b/src/components/input-errors/input-errors.ts
@@ -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:
+ *
+ *
+ * {{ 'mm.login.username' | translate }}
+ *
+ *
+ *
+ */
+@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');
+ // }
+ // }
+ // });
+ }
+
+}
diff --git a/src/components/mark-required/mark-required.html b/src/components/mark-required/mark-required.html
new file mode 100644
index 000000000..b50d08d78
--- /dev/null
+++ b/src/components/mark-required/mark-required.html
@@ -0,0 +1,2 @@
+
+
diff --git a/src/components/mark-required/mark-required.scss b/src/components/mark-required/mark-required.scss
new file mode 100644
index 000000000..5bff71c3f
--- /dev/null
+++ b/src/components/mark-required/mark-required.scss
@@ -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;
+ }
+}
diff --git a/src/components/mark-required/mark-required.ts b/src/components/mark-required/mark-required.ts
new file mode 100644
index 000000000..f41614110
--- /dev/null
+++ b/src/components/mark-required/mark-required.ts
@@ -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:
+ *
+ * {{ 'mm.login.username' | translate }}
+ */
+@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, ''));
+ }
+ }
+ }
+}
diff --git a/src/components/show-password/show-password.html b/src/components/show-password/show-password.html
new file mode 100644
index 000000000..440aa125f
--- /dev/null
+++ b/src/components/show-password/show-password.html
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/components/show-password/show-password.scss b/src/components/show-password/show-password.scss
new file mode 100644
index 000000000..1beb78e94
--- /dev/null
+++ b/src/components/show-password/show-password.scss
@@ -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;
+ }
+}
diff --git a/src/components/show-password/show-password.ts b/src/components/show-password/show-password.ts
new file mode 100644
index 000000000..0df6189fc
--- /dev/null
+++ b/src/components/show-password/show-password.ts
@@ -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:
+ *
+ *
+ *
+ *
+ */
+@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 = 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();
+ }
+}
diff --git a/src/directives/auto-focus.ts b/src/directives/auto-focus.ts
index 13ad67418..cc3745d6f 100644
--- a/src/directives/auto-focus.ts
+++ b/src/directives/auto-focus.ts
@@ -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') {
diff --git a/src/directives/directives.module.ts b/src/directives/directives.module.ts
index 5f10d4699..105f6565b 100644
--- a/src/directives/directives.module.ts
+++ b/src/directives/directives.module.ts
@@ -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
]
})
diff --git a/src/directives/keep-keyboard.ts b/src/directives/keep-keyboard.ts
new file mode 100644
index 000000000..e464ed15e
--- /dev/null
+++ b/src/directives/keep-keyboard.ts
@@ -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:
+ *
+ *
+ *
+ *
+ * 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:
+ *
+ *
+ *
+ */
+@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();
+ }
+}