From f3e5c21b6e017217b458b7494e3e0e2abe751cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 27 Apr 2020 13:43:38 +0200 Subject: [PATCH] MOBILE-3394 utils: Improve alerts and prompts --- src/core/login/pages/site/site.ts | 19 +- src/core/viewer/pages/textarea/textarea.html | 14 ++ .../viewer/pages/textarea/textarea.module.ts | 36 +++ src/core/viewer/pages/textarea/textarea.scss | 187 ++++++++++++++++ src/core/viewer/pages/textarea/textarea.ts | 71 ++++++ src/providers/utils/dom.ts | 208 +++++++++--------- 6 files changed, 421 insertions(+), 114 deletions(-) create mode 100644 src/core/viewer/pages/textarea/textarea.html create mode 100644 src/core/viewer/pages/textarea/textarea.module.ts create mode 100644 src/core/viewer/pages/textarea/textarea.scss create mode 100644 src/core/viewer/pages/textarea/textarea.ts diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index eec309a4c..220e055d3 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -277,7 +277,11 @@ export class CoreLoginSitePage { } ]; - this.domUtils.showAlertWithButtons(this.translate.instant('core.cannotconnect'), message, buttons); + this.domUtils.showAlertWithOptions({ + title: this.translate.instant('core.cannotconnect'), + message, + buttons, + }); } /** @@ -369,10 +373,11 @@ export class CoreLoginSitePage { */ showInstructionsAndScanQR(): void { // Show some instructions first. - this.domUtils.showAlertWithButtons( - this.translate.instant('core.login.faqwhereisqrcode'), - this.translate.instant('core.login.faqwhereisqrcodeanswer', {$image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML}), - [ + this.domUtils.showAlertWithOptions({ + title: this.translate.instant('core.login.faqwhereisqrcode'), + message: this.translate.instant('core.login.faqwhereisqrcodeanswer', + {$image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML}), + buttons: [ { text: this.translate.instant('core.cancel'), role: 'cancel' @@ -383,8 +388,8 @@ export class CoreLoginSitePage { this.scanQR(); } }, - ] - ); + ], + }); } /** diff --git a/src/core/viewer/pages/textarea/textarea.html b/src/core/viewer/pages/textarea/textarea.html new file mode 100644 index 000000000..d43f4a11e --- /dev/null +++ b/src/core/viewer/pages/textarea/textarea.html @@ -0,0 +1,14 @@ + + + {{ title }} + + + +
+ +
+ +
+
diff --git a/src/core/viewer/pages/textarea/textarea.module.ts b/src/core/viewer/pages/textarea/textarea.module.ts new file mode 100644 index 000000000..a17d8cc8e --- /dev/null +++ b/src/core/viewer/pages/textarea/textarea.module.ts @@ -0,0 +1,36 @@ +// (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 { TranslateModule } from '@ngx-translate/core'; +import { CoreViewerTextAreaPage } from './textarea'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +/** + * Module to lazy load the page. + */ +@NgModule({ + declarations: [ + CoreViewerTextAreaPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreViewerTextAreaPage), + TranslateModule.forChild() + ] +}) +export class CoreViewerTextAreaPageModule {} diff --git a/src/core/viewer/pages/textarea/textarea.scss b/src/core/viewer/pages/textarea/textarea.scss new file mode 100644 index 000000000..27a2ea061 --- /dev/null +++ b/src/core/viewer/pages/textarea/textarea.scss @@ -0,0 +1,187 @@ +$core-modal-promt-min-width: 320px; + +ion-app.app-root ion-modal.core-modal-prompt { + /* Some styles have been copied from ionic alert component. */ + @include position(0, 0, 0, 0); + position: absolute; + z-index: $z-index-overlay; + display: flex; + align-items: center; + justify-content: center; + contain: strict; + + ion-backdrop { + visibility: visible; + } + + .header { + &::after { + background: none; + } + .toolbar-background { + display: none; + } + } + + .prompt-button-group { + display: flex; + flex-direction: row; + + .prompt-button { + @include margin(0); + + z-index: 0; + display: block; + + font-size: $alert-button-font-size; + line-height: $alert-button-line-height; + } + } + + ion-textarea { + @include placeholder($alert-input-placeholder-color); + @include padding($alert-md-message-padding-top, $alert-md-message-padding-end, $alert-md-message-padding-bottom, $alert-md-message-padding-start); + border: 0; + background: inherit; + + textarea { + margin: 0; + } + } + + .prompt-message { + @include deprecated-variable(padding, $alert-md-message-padding) { + @include padding($alert-md-message-padding-top, $alert-md-message-padding-end, $alert-md-message-padding-bottom, $alert-md-message-padding-start); + } + } + + .modal-wrapper { + z-index: $z-index-overlay-wrapper; + display: flex; + flex-direction: column; + min-width: $core-modal-promt-min-width; + max-height: $alert-max-height; + opacity: 0; + contain: content; + height: auto; + + page-core-viewer-textarea, + ion-content, + .fixed-content, + .scroll-content { + position: relative; + background: $white; + overflow: hidden; + } + .fixed-content { + display: none; + } + .scroll-content { + padding: 0 !important; + } + } + + .content-md .prompt-button-group { + flex-wrap: $alert-md-button-group-flex-wrap; + justify-content: $alert-md-button-group-justify-content; + + @include deprecated-variable(padding, $alert-md-button-group-padding) { + @include padding($alert-md-button-group-padding-top, $alert-md-button-group-padding-end, $alert-md-button-group-padding-bottom, $alert-md-button-group-padding-start); + } + + .prompt-button { + @include text-align($alert-md-button-text-align); + @include border-radius($alert-md-button-border-radius); + + // necessary for ripple to work properly + position: relative; + overflow: hidden; + + font-weight: $alert-md-button-font-weight; + text-transform: $alert-md-button-text-transform; + color: $alert-md-button-text-color; + background-color: $alert-md-button-background-color; + + @include deprecated-variable(margin, $alert-md-button-margin) { + @include margin($alert-md-button-margin-top, $alert-md-button-margin-end, $alert-md-button-margin-bottom, $alert-md-button-margin-start); + } + + @include deprecated-variable(padding, $alert-md-button-padding) { + @include padding($alert-md-button-padding-top, $alert-md-button-padding-end, $alert-md-button-padding-bottom, $alert-md-button-padding-start); + } + } + + .prompt-button.activated { + background-color: $alert-md-button-background-color-activated; + } + + .prompt-button .button-inner { + justify-content: $alert-md-button-group-justify-content; + } + } + + .content-ios .prompt-button-group { + @include margin-horizontal(null, -$alert-ios-button-border-width); + + flex-wrap: $alert-ios-button-group-flex-wrap; + .prompt-button { + @include margin($alert-ios-button-margin); + @include border-radius($alert-ios-button-border-radius); + + overflow: hidden; + + flex: $alert-ios-button-flex; + + min-width: $alert-ios-button-min-width; + height: $alert-ios-button-min-height; + + border-top: $alert-ios-button-border-width $alert-ios-button-border-style $alert-ios-button-border-color; + border-right: $alert-ios-button-border-width $alert-ios-button-border-style $alert-ios-button-border-color; + font-size: $alert-ios-button-font-size; + color: $alert-ios-button-text-color; + background-color: $alert-ios-button-background-color; + } + + .prompt-button:last-child { + border-right: 0; + font-weight: $alert-ios-button-main-font-weight; + } + + .prompt-button.activated { + background-color: $alert-ios-button-background-color-activated; + } + } +} + +ion-app.app-root-md ion-modal.core-modal-prompt { + .modal-wrapper { + @include border-radius($alert-md-border-radius); + max-width: $alert-md-max-width; + background-color: $alert-md-background-color; + box-shadow: $alert-md-box-shadow; + } + + .toolbar-content .toolbar-title { + color: $alert-md-message-text-color; + white-space: normal; + } +} + +ion-app.app-root-ios ion-modal.core-modal-prompt { + .modal-wrapper { + @include border-radius($alert-ios-border-radius); + overflow: hidden; + max-width: $alert-ios-max-width; + background-color: $alert-ios-background; + box-shadow: $alert-ios-box-shadow; + } + + .toolbar-content .toolbar-title { + color: $alert-ios-message-text-color; + white-space: normal; + } + + ion-title { + padding: 0; + } +} \ No newline at end of file diff --git a/src/core/viewer/pages/textarea/textarea.ts b/src/core/viewer/pages/textarea/textarea.ts new file mode 100644 index 000000000..e01e418be --- /dev/null +++ b/src/core/viewer/pages/textarea/textarea.ts @@ -0,0 +1,71 @@ +// (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, AlertButton } from 'ionic-angular'; + +/** + * Page to render a textarea prompt. + */ +@IonicPage({ segment: 'core-viewer-textarea' }) +@Component({ + selector: 'page-core-viewer-textarea', + templateUrl: 'textarea.html', +}) +export class CoreViewerTextAreaPage { + title: string; + message: string; + placeholder: string; + buttons: AlertButton[]; + text = ''; + + constructor( + protected viewCtrl: ViewController, + params: NavParams, + ) { + this.title = params.get('title'); + this.message = params.get('message'); + this.placeholder = params.get('placeholder') || ''; + + const buttons = params.get('buttons'); + + this.buttons = buttons.map((button) => { + if (typeof button === 'string') { + return { text: button }; + } + + return button; + }); + } + + /** + * Button clicked. + * + * @param button: Clicked button. + */ + buttonClicked(button: AlertButton): void { + let shouldDismiss = true; + if (button.handler) { + // A handler has been provided, execute it pass the handler the values from the inputs + if (button.handler(this.text) === false) { + // If the return value of the handler is false then do not dismiss + shouldDismiss = false; + } + } + + if (shouldDismiss) { + this.viewCtrl.dismiss(button.role); + } + } +} diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index b4c904f9a..9ef11f960 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -15,7 +15,7 @@ import { Injectable, SimpleChange, ElementRef } from '@angular/core'; import { LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform, Content, PopoverController, - ModalController, AlertButton + ModalController, AlertButton, AlertOptions } from 'ionic-angular'; import { DomSanitizer } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; @@ -1139,41 +1139,36 @@ export class CoreDomUtilsProvider { * @return Promise resolved with the alert modal. */ async showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise { - const buttons = [buttonText || this.translate.instant('core.ok')]; - - return this.showAlertWithButtons(title, message, buttons, autocloseTime); + return this.showAlertWithOptions({ + title: title, + message, + buttons: [buttonText || this.translate.instant('core.ok')] + }, autocloseTime); } /** - * Show an alert modal with some buttons. + * General show an alert modal. * - * @param title Title to show. - * @param message Message to show. - * @param buttons Buttons objects or texts. + * @param options Alert options to pass to the alert. * @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 { - const hasHTMLTags = this.textUtils.hasHTMLTags(message); + async showAlertWithOptions(options: AlertOptions = {}, autocloseTime?: number): Promise { + const hasHTMLTags = this.textUtils.hasHTMLTags(options.message || ''); if (hasHTMLTags) { // Format the text. - message = await this.textUtils.formatText(message); + options.message = await this.textUtils.formatText(options.message); } - const alertId = Md5.hashAsciiStr((title || '') + '#' + (message || '')); + const alertId = Md5.hashAsciiStr((options.title || '') + '#' + (options.message || '')); if (this.displayedAlerts[alertId]) { // There's already an alert with the same message and title. Return it. return this.displayedAlerts[alertId]; } - const alert: CoreAlert = this.alertCtrl.create({ - title: title, - message: message, - buttons: buttons, - }); + const alert: CoreAlert = this.alertCtrl.create(options); alert.present().then(() => { if (hasHTMLTags) { @@ -1202,8 +1197,18 @@ export class CoreDomUtilsProvider { }); if (autocloseTime > 0) { - setTimeout(() => { - alert.dismiss(); + setTimeout(async () => { + await alert.dismiss(); + + if (options.buttons) { + // Execute dismiss function if any. + const cancelButton = options.buttons.find((button) => { + return typeof button != 'string' && typeof button.role != 'undefined' && + typeof button.handler != 'undefined' && button.role == 'cancel'; + }); + cancelButton && cancelButton.handler(null); + } + }, autocloseTime); } @@ -1250,52 +1255,33 @@ export class CoreDomUtilsProvider { * @param options More options. See https://ionicframework.com/docs/v3/api/components/alert/AlertController/ * @return Promise resolved if the user confirms and rejected with a canceled error if he cancels. */ - showConfirm(message: string, title?: string, okText?: string, cancelText?: string, options?: any): Promise { + showConfirm(message: string, title?: string, okText?: string, cancelText?: string, options: AlertOptions = {}): Promise { return new Promise((resolve, reject): void => { - const hasHTMLTags = this.textUtils.hasHTMLTags(message); - let promise; - if (hasHTMLTags) { - // Format the text. - promise = this.textUtils.formatText(message); - } else { - promise = Promise.resolve(message); + options.title = title; + options.message = message; + + options.buttons = [ + { + text: cancelText || this.translate.instant('core.cancel'), + role: 'cancel', + handler: (): void => { + reject(this.createCanceledError()); + } + }, + { + text: okText || this.translate.instant('core.ok'), + handler: (data: any): void => { + resolve(data); + } + } + ]; + + if (!title) { + options.cssClass = 'core-nohead'; } - promise.then((message) => { - options = options || {}; - - options.message = message; - options.title = title; - if (!title) { - options.cssClass = 'core-nohead'; - } - options.buttons = [ - { - text: cancelText || this.translate.instant('core.cancel'), - role: 'cancel', - handler: (): void => { - reject(this.createCanceledError()); - } - }, - { - text: okText || this.translate.instant('core.ok'), - handler: (data: any): void => { - resolve(data); - } - } - ]; - - const alert = this.alertCtrl.create(options); - - 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); - } - }); - }); + this.showAlertWithOptions(options, 0); }); } @@ -1414,59 +1400,67 @@ export class CoreDomUtilsProvider { * @param title Modal title. * @param placeholder Placeholder of the input element. By default, "Password". * @param type Type of the input element. By default, password. + * @param options More options to pass to the alert. * @return Promise resolved with the input data if the user clicks OK, rejected if cancels. */ showPrompt(message: string, title?: string, placeholder?: string, type: string = 'password'): Promise { - return new Promise((resolve, reject): void => { - const hasHTMLTags = this.textUtils.hasHTMLTags(message); - let promise; + return new Promise((resolve, reject): any => { + placeholder = typeof placeholder == 'undefined' || placeholder == null ? + this.translate.instant('core.login.password') : placeholder; - if (hasHTMLTags) { - // Format the text. - promise = this.textUtils.formatText(message); - } else { - promise = Promise.resolve(message); - } - - promise.then((message) => { - const alert = this.alertCtrl.create({ - message: message, - title: title, - inputs: [ - { - name: 'promptinput', - placeholder: placeholder || this.translate.instant('core.login.password'), - type: type - } - ], - buttons: [ - { - text: this.translate.instant('core.cancel'), - role: 'cancel', - handler: (): void => { - reject(); - } - }, - { - text: this.translate.instant('core.ok'), - handler: (data): void => { - resolve(data.promptinput); - } - } - ] - }); - - 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); + const options: AlertOptions = { + title, + message, + inputs: [ + { + name: 'promptinput', + placeholder: placeholder, + type: type } - }); - }); + ], + buttons: [ + { + text: this.translate.instant('core.cancel'), + role: 'cancel', + handler: (): void => { + reject(); + } + }, + { + text: this.translate.instant('core.ok'), + handler: (data): void => { + resolve(data.promptinput); + } + } + ], + }; + + this.showAlertWithOptions(options); }); } + /** + * Show a prompt modal to input a textarea. + * + * @param title Modal title. + * @param message Modal message. + * @param buttons Buttons to pass to the modal. + * @param placeholder Placeholder of the input element if any. + * @return Promise resolved when modal presented. + */ + showTextareaPrompt(title: string, message: string, buttons: (string | AlertButton)[], placeholder?: string): Promise { + const params = { + title: title, + message: message, + placeholder: placeholder, + buttons: buttons, + }; + + const modal = this.modalCtrl.create('CoreViewerTextAreaPage', params, { cssClass: 'core-modal-prompt' }); + + return modal.present(); + } + /** * Displays an autodimissable toast modal window. *