From d250e661f3085c511636f92eda82b236e03d2419 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 13 Feb 2018 07:21:20 -0800 Subject: [PATCH] Service to show confirmation prompt when leaving a dirty form (#16688) * Service to show confirmation prompt when leaving a dirty form * Add data test subject for breadcrumbs This will enable testing the dirty prompt service via functional tests by providing a means for the user to navigate away via the breadcrumbs --- .../dirty_prompt/dirty_prompt.factory.js | 11 +++ src/ui/public/dirty_prompt/dirty_prompt.js | 80 +++++++++++++++++++ src/ui/public/dirty_prompt/index.js | 1 + .../bread_crumbs/bread_crumbs.html | 1 + 4 files changed, 93 insertions(+) create mode 100644 src/ui/public/dirty_prompt/dirty_prompt.factory.js create mode 100644 src/ui/public/dirty_prompt/dirty_prompt.js create mode 100644 src/ui/public/dirty_prompt/index.js diff --git a/src/ui/public/dirty_prompt/dirty_prompt.factory.js b/src/ui/public/dirty_prompt/dirty_prompt.factory.js new file mode 100644 index 0000000000000..cbe01bdc4401f --- /dev/null +++ b/src/ui/public/dirty_prompt/dirty_prompt.factory.js @@ -0,0 +1,11 @@ +import { uiModules } from 'ui/modules'; +import { DirtyPrompt } from './dirty_prompt'; + +uiModules.get('kibana') + .factory('dirtyPrompt', ($injector) => { + const $window = $injector.get('$window'); + const confirmModal = $injector.get('confirmModal'); + const $rootScope = $injector.get('$rootScope'); + + return new DirtyPrompt($window, $rootScope, confirmModal); + }); diff --git a/src/ui/public/dirty_prompt/dirty_prompt.js b/src/ui/public/dirty_prompt/dirty_prompt.js new file mode 100644 index 0000000000000..f2bc1f4877c72 --- /dev/null +++ b/src/ui/public/dirty_prompt/dirty_prompt.js @@ -0,0 +1,80 @@ +import { noop } from 'lodash'; + +const confirmMessage = `You have unsaved changes. Proceed and discard changes?`; + +function registerUrlChangeHandler(checkDirty) { + this.beforeUnloadHandler = (event) => { + if (checkDirty()) { + // Browsers do not honor the message you set here. The only requirement + // is that is is not an empty string. I am just using the confirmMessage + // here for consistency + event.returnValue = confirmMessage; + } + }; + + // When the user navigates to an external url or another app, we must + // rely on the build-in beforeunload confirmation dialog. We do not have + // the ability to change the text or appearance of this dialog. + this.$window.addEventListener('beforeunload', this.beforeUnloadHandler); +} + +function deregisterUrlChangeHandler() { + this.$window.removeEventListener('beforeunload', this.beforeUnloadHandler); +} + +function registerRouteChangeHandler(checkDirty) { + // When the user navigates within the same app, we can present them with + // a friendly confirmation dialog box + const deregister = this.$rootScope.$on('$locationChangeStart', (event, newUrl) => { + if (!checkDirty()) { + return; + } + + // At this point, we know the dirty prompt should be shown, so + // cancel the location change event, and keep the user at + // their current location + event.preventDefault(); + + // Notify user about unsaved changes and ask the user for confirmation + // about navigating away (changing their location) anyway + const confirmModalOptions = { + onConfirm: () => { + this.deregister(); + this.$window.location.href = newUrl; + }, + confirmButtonText: 'Discard Changes' + }; + + return this.confirmModal(confirmMessage, confirmModalOptions); + }); + + this.deregisterListener = deregister; +} + +function deregisterRouteChangeHandler() { + this.deregisterListener(); +} + +export class DirtyPrompt { + constructor($window, $rootScope, confirmModal) { + this.$window = $window; + this.$rootScope = $rootScope; + this.confirmModal = confirmModal; + this.deregisterListener = noop; + this.beforeUnloadHandler = noop; + } + + /** + * @param checkDirty function which returns a bool to call to + * determine dirty state + */ + register = (checkDirty) => { + registerUrlChangeHandler.call(this, checkDirty); + registerRouteChangeHandler.call(this, checkDirty); + } + + deregister = () => { + deregisterUrlChangeHandler.call(this); + deregisterRouteChangeHandler.call(this); + } +} diff --git a/src/ui/public/dirty_prompt/index.js b/src/ui/public/dirty_prompt/index.js new file mode 100644 index 0000000000000..736fa8521f771 --- /dev/null +++ b/src/ui/public/dirty_prompt/index.js @@ -0,0 +1 @@ +import './dirty_prompt.factory'; diff --git a/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.html b/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.html index 6cefb688c1be7..bddd6f840a18f 100644 --- a/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.html +++ b/src/ui/public/kbn_top_nav/bread_crumbs/bread_crumbs.html @@ -12,6 +12,7 @@ {{ breadcrumb.display }}