├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── UPGRADING.md ├── addon └── mixins │ └── confirmation.js ├── app └── .gitkeep ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js ├── tests ├── .eslintrc.js ├── acceptance │ └── smoke-test.js ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ └── .gitkeep │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ ├── foo.js │ │ │ └── index.js │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ ├── application.hbs │ │ │ ├── components │ │ │ └── .gitkeep │ │ │ ├── foo.hbs │ │ │ └── index.hbs │ ├── config │ │ ├── environment.js │ │ └── targets.js │ └── public │ │ └── robots.txt ├── helpers │ ├── .gitkeep │ ├── destroy-app.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── test-helper.js └── unit │ ├── .gitkeep │ └── mixins │ └── confirmation-test.js ├── vendor └── .gitkeep └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | ecmaVersion: 2017, 6 | sourceType: 'module' 7 | }, 8 | plugins: [ 9 | 'ember' 10 | ], 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:ember/recommended' 14 | ], 15 | env: { 16 | browser: true 17 | }, 18 | rules: { 19 | 'ember/no-jquery': 'error' 20 | }, 21 | overrides: [ 22 | // node files 23 | { 24 | files: [ 25 | 'ember-cli-build.js', 26 | 'index.js', 27 | 'testem.js', 28 | 'config/**/*.js', 29 | 'tests/dummy/config/**/*.js' 30 | ], 31 | excludedFiles: [ 32 | 'addon/**', 33 | 'addon-test-support/**', 34 | 'app/**', 35 | 'tests/dummy/app/**' 36 | ], 37 | parserOptions: { 38 | sourceType: 'script' 39 | }, 40 | env: { 41 | browser: false, 42 | node: true 43 | }, 44 | plugins: ['node'], 45 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 46 | // add your custom rules and overrides for node files here 47 | }) 48 | } 49 | ] 50 | }; 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log* 17 | yarn-error.log 18 | testem.log 19 | 20 | # ember-try 21 | .node_modules.ember-try/ 22 | bower.json.ember-try 23 | package.json.ember-try 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | /bower_components 3 | ======= 4 | # compiled output 5 | /dist/ 6 | /tmp/ 7 | 8 | # dependencies 9 | /bower_components/ 10 | 11 | # misc 12 | /.bowerrc 13 | /.editorconfig 14 | /.ember-cli 15 | /.env* 16 | /.eslintignore 17 | /.eslintrc.js 18 | /.git/ 19 | /.gitignore 20 | /.template-lintrc.js 21 | /.travis.yml 22 | /.watchmanconfig 23 | /bower.json 24 | >>>>>>> 4eea44a... message 25 | /config/ember-try.js 26 | /dist 27 | /tests 28 | /tmp 29 | **/.gitkeep 30 | .bowerrc 31 | .editorconfig 32 | .ember-cli 33 | .eslintrc.js 34 | .gitignore 35 | .watchmanconfig 36 | .travis.yml 37 | bower.json 38 | ember-cli-build.js 39 | testem.js 40 | 41 | # ember-try 42 | .node_modules.ember-try/ 43 | bower.json.ember-try 44 | package.json.ember-try 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "8" 7 | 8 | sudo: false 9 | dist: trusty 10 | 11 | addons: 12 | chrome: stable 13 | 14 | cache: 15 | directories: 16 | - $HOME/.npm 17 | 18 | env: 19 | global: 20 | # See https://git.io/vdao3 for details. 21 | - JOBS=1 22 | 23 | branches: 24 | only: 25 | - master 26 | # npm version tags 27 | - /^v\d+\.\d+\.\d+/ 28 | 29 | jobs: 30 | fail_fast: true 31 | allow_failures: 32 | - env: EMBER_TRY_SCENARIO=ember-canary 33 | 34 | include: 35 | # runs linting and tests with current locked deps 36 | 37 | - stage: "Tests" 38 | name: "Tests" 39 | script: 40 | - npm run lint:js 41 | - npm test 42 | 43 | # we recommend new addons test the current and previous LTS 44 | # as well as latest stable release (bonus points to beta/canary) 45 | - stage: "Additional Tests" 46 | env: EMBER_TRY_SCENARIO=ember-lts-3.4 47 | - env: EMBER_TRY_SCENARIO=ember-lts-3.8 48 | - env: EMBER_TRY_SCENARIO=ember-release 49 | - env: EMBER_TRY_SCENARIO=ember-beta 50 | - env: EMBER_TRY_SCENARIO=ember-canary 51 | - env: EMBER_TRY_SCENARIO=ember-default-with-jquery 52 | 53 | script: 54 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 55 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.0.0 (2019-11-06) 2 | 3 | #### :boom: Breaking Change 4 | * [#23](https://github.com/jasonmit/ember-onbeforeunload/pull/23) Upgrade to ember 3.14 ([@tylerturdenpants](https://github.com/tylerturdenpants)) 5 | 6 | #### :rocket: Enhancement 7 | * [#30](https://github.com/jasonmit/ember-onbeforeunload/pull/30) Add release-it ([@tylerturdenpants](https://github.com/tylerturdenpants)) 8 | * [#18](https://github.com/jasonmit/ember-onbeforeunload/pull/18) Add support for custom async confirmation handler ([@krasnoukhov](https://github.com/krasnoukhov)) 9 | 10 | #### Committers: 2 11 | - Dmitry Krasnoukhov ([@krasnoukhov](https://github.com/krasnoukhov)) 12 | - Ryan Mark ([@tylerturdenpants](https://github.com/tylerturdenpants)) 13 | 14 | # Change Log 15 | 16 | ## [1.2.0](https://github.com/jasonmit/ember-onbeforeunload/tree/1.2.0) (2018-09-04) 17 | 18 | [Full Changelog](https://github.com/jasonmit/ember-onbeforeunload/compare/1.1.2...1.2.0) 19 | 20 | **Merged pull requests:** 21 | 22 | - Update to ember 2.16 LTS [\#17](https://github.com/jasonmit/ember-onbeforeunload/pull/17) ([blimmer](https://github.com/blimmer)) 23 | 24 | ## [1.1.2](https://github.com/jasonmit/ember-onbeforeunload/tree/1.1.2) (2017-08-13) 25 | 26 | [Full Changelog](https://github.com/jasonmit/ember-onbeforeunload/compare/1.0.0...1.1.2) 27 | 28 | **Merged pull requests:** 29 | 30 | - Upgrade ember-cli [\#16](https://github.com/jasonmit/ember-onbeforeunload/pull/16) ([blimmer](https://github.com/blimmer)) 31 | - Upgrade ember CLI to 2.9.1 [\#14](https://github.com/jasonmit/ember-onbeforeunload/pull/14) ([blimmer](https://github.com/blimmer)) 32 | 33 | ## [1.0.0](https://github.com/jasonmit/ember-onbeforeunload/tree/1.0.0) (2016-05-15) 34 | 35 | [Full Changelog](https://github.com/jasonmit/ember-onbeforeunload/compare/0.3.0...1.0.0) 36 | 37 | **Closed issues:** 38 | 39 | - canUnload and allowUnload is confusing [\#12](https://github.com/jasonmit/ember-onbeforeunload/issues/12) 40 | - Removing controller.isDirty default logic [\#10](https://github.com/jasonmit/ember-onbeforeunload/issues/10) 41 | - Add Automated Tests [\#8](https://github.com/jasonmit/ember-onbeforeunload/issues/8) 42 | 43 | **Merged pull requests:** 44 | 45 | - 1.0.0 Release [\#13](https://github.com/jasonmit/ember-onbeforeunload/pull/13) ([blimmer](https://github.com/blimmer)) 46 | 47 | ## [0.3.0](https://github.com/jasonmit/ember-onbeforeunload/tree/0.3.0) (2016-05-05) 48 | 49 | [Full Changelog](https://github.com/jasonmit/ember-onbeforeunload/compare/0.2.0...0.3.0) 50 | 51 | **Closed issues:** 52 | 53 | - Turn on TravisCI [\#9](https://github.com/jasonmit/ember-onbeforeunload/issues/9) 54 | 55 | **Merged pull requests:** 56 | 57 | - Release 0.3.0. [\#11](https://github.com/jasonmit/ember-onbeforeunload/pull/11) ([blimmer](https://github.com/blimmer)) 58 | - Exception if ConfirmationMixin not used on Route. [\#7](https://github.com/jasonmit/ember-onbeforeunload/pull/7) ([blimmer](https://github.com/blimmer)) 59 | - Upgrade to ember-cli 2.5.0 [\#6](https://github.com/jasonmit/ember-onbeforeunload/pull/6) ([blimmer](https://github.com/blimmer)) 60 | - Correct mixin location in README [\#5](https://github.com/jasonmit/ember-onbeforeunload/pull/5) ([ronco](https://github.com/ronco)) 61 | - Upgrade ember-cli version & standardize mixin location [\#4](https://github.com/jasonmit/ember-onbeforeunload/pull/4) ([ronco](https://github.com/ronco)) 62 | 63 | ## [0.2.0](https://github.com/jasonmit/ember-onbeforeunload/tree/0.2.0) (2015-12-07) 64 | 65 | [Full Changelog](https://github.com/jasonmit/ember-onbeforeunload/compare/0.1.0...0.2.0) 66 | 67 | ## [0.1.0](https://github.com/jasonmit/ember-onbeforeunload/tree/0.1.0) (2015-07-24) 68 | 69 | [Full Changelog](https://github.com/jasonmit/ember-onbeforeunload/compare/0.0.1...0.1.0) 70 | 71 | **Closed issues:** 72 | 73 | - Missing license [\#2](https://github.com/jasonmit/ember-onbeforeunload/issues/2) 74 | 75 | **Merged pull requests:** 76 | 77 | - turn it into an ember addon [\#3](https://github.com/jasonmit/ember-onbeforeunload/pull/3) ([jasonmit](https://github.com/jasonmit)) 78 | 79 | ## [0.0.1](https://github.com/jasonmit/ember-onbeforeunload/tree/0.0.1) (2015-06-21) 80 | 81 | [Full Changelog](https://github.com/jasonmit/ember-onbeforeunload/compare/78e4e0c3ac32ff7d14ef008aeb56804771681645...0.0.1) 82 | 83 | **Closed issues:** 84 | 85 | - use controller as the source of truth for isDirty instead of currentModel [\#1](https://github.com/jasonmit/ember-onbeforeunload/issues/1) 86 | 87 | 88 | 89 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-onbeforeunload 2 | [![npm Version][npm-badge]][npm] 3 | [![Build Status][travis-badge]][travis] 4 | [![Ember Observer Score](https://emberobserver.com/badges/ember-onbeforeunload.svg)](https://emberobserver.com/addons/ember-onbeforeunload) 5 | 6 | An add-on to conditionally prompt the user when transitioning between routes or closing the browser. 7 | 8 | ## Installation 9 | This library is tested against Ember 1.13.x and Ember 2.x. 10 | 11 | ``` 12 | ember install ember-onbeforeunload 13 | ``` 14 | 15 | ## Usage 16 | To get started, mix the `ConfirmationMixin` into your `Ember.Route`. By default, 17 | the user will be prompted beforeunload any time the `model` for the route 18 | `hasDirtyAttributes` ([docs](http://emberjs.com/api/data/classes/DS.Model.html#property_hasDirtyAttributes)). 19 | 20 | ```js 21 | // app/routes/foo.js 22 | import Ember from 'ember'; 23 | import ConfirmationMixin from 'ember-onbeforeunload/mixins/confirmation'; 24 | 25 | export default Ember.Route.extend(ConfirmationMixin, { }); 26 | ``` 27 | 28 | ## Customization 29 | This addon tries to provide sane defaults, but it also exposes all of the internals 30 | for customization. 31 | 32 | ### Confirmation Message 33 | You can customize the message displayed in the confirmation dialog by overriding 34 | the `confirmationMessage` property. You can either pass a hard-coded string, 35 | or use a function. 36 | 37 | ```javascript 38 | export default Ember.Route.extend(ConfirmationMixin, { 39 | confirmationMessage: 'Are you sure?', 40 | }); 41 | ``` 42 | 43 | ```javascript 44 | export default Ember.Route.extend(ConfirmationMixin, { 45 | confirmationMessage(model) { 46 | return `Are you sure you want to unload ${model.name}?`; 47 | } 48 | }); 49 | ``` 50 | 51 | ```javascript 52 | export default Ember.Route.extend(ConfirmationMixin, { 53 | i18n: service(), // see ember-i18n 54 | confirmationMessage() { 55 | return this.get('i18n').t('myTranslation'); 56 | } 57 | }); 58 | ``` 59 | 60 | ### isPageDirty logic 61 | If you do not sure Ember Data, or you have other logic to determine whether or 62 | not the page is dirty, you can override the `isPageDirty` method. 63 | 64 | ```javascript 65 | export default Ember.Route.extend(ConfirmationMixin, { 66 | isPageDirty(/* model */) { 67 | const isDirty = true; // your logic here 68 | return isDirty; 69 | } 70 | }); 71 | ``` 72 | 73 | ### Allow Dirty Transitions 74 | By default, we allow navigating within the hierarchy of route you mix the 75 | `ConfirmationMixin` into. For example, navigating from `myroute.index` to 76 | `myroute.index.subroute` would not check `isPageDirty`. If you have other logic 77 | that determines when a dirty transition should be allowed, you can override 78 | `shouldCheckIsPageDirty`. 79 | 80 | ```javascript 81 | export default Ember.Route.extend(ConfirmationMixin, { 82 | shouldCheckIsPageDirty(transition) { 83 | const isChildRouteTransition = this._super(...arguments); 84 | 85 | if (transition.targetName === 'some-exempt-route') { 86 | return true; 87 | } else { 88 | return isChildRouteTransition; 89 | } 90 | } 91 | }); 92 | ``` 93 | 94 | ### onUnload logic 95 | If you have some custom logic you'd like to execute when your route is unloaded, 96 | you can tie into the `onUnload` function. By default, this function is a no-op. 97 | 98 | ```javascript 99 | export default Ember.Route.extend(ConfirmationMixin, { 100 | onUnload() { 101 | // my custom unload logic 102 | } 103 | }); 104 | ``` 105 | 106 | # Upgrading 107 | This library underwent major API changes with version 1.0.0. For information on 108 | how to upgrade, please check out [UPGRADING.md](https://github.com/jasonmit/ember-onbeforeunload/blob/master/UPGRADING.md). 109 | 110 | # Issues 111 | Found a bug? Please report it! 112 | 113 | # Development Instructions 114 | 115 | ## Installing 116 | * `git clone` this repository 117 | * `npm install` 118 | * `bower install` 119 | 120 | ### Linting 121 | 122 | * `npm run lint:js` 123 | * `npm run lint:js -- --fix` 124 | 125 | ### Running tests 126 | 127 | * `ember test` – Runs the test suite on the current Ember version 128 | * `ember test --server` – Runs the test suite in "watch mode" 129 | * `npm test` – Runs `ember try:each` to test your addon against multiple Ember versions 130 | 131 | ### Running the dummy application 132 | 133 | * `ember serve` 134 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 135 | 136 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 137 | 138 | [npm]: https://www.npmjs.org/package/ember-onbeforeunload 139 | [npm-badge]: https://img.shields.io/npm/v/ember-onbeforeunload.svg?style=flat-square 140 | [travis]: https://travis-ci.org/jasonmit/ember-onbeforeunload 141 | [travis-badge]: https://img.shields.io/travis/jasonmit/ember-onbeforeunload.svg?branch=master&style=flat-square 142 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | 8 | ## Preparation 9 | 10 | Since the majority of the actual release process is automated, the primary 11 | remaining task prior to releasing is confirming that all pull requests that 12 | have been merged since the last release have been labeled with the appropriate 13 | `lerna-changelog` labels and the titles have been updated to ensure they 14 | represent something that would make sense to our users. Some great information 15 | on why this is important can be found at 16 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 17 | guiding principles here is that changelogs are for humans, not machines. 18 | 19 | When reviewing merged PR's the labels to be used are: 20 | 21 | * breaking - Used when the PR is considered a breaking change. 22 | * enhancement - Used when the PR adds a new feature or enhancement. 23 | * bug - Used when the PR fixes a bug included in a previous release. 24 | * documentation - Used when the PR adds or updates documentation. 25 | * internal - Used for internal changes that still require a mention in the 26 | changelog/release notes. 27 | 28 | 29 | ## Release 30 | 31 | Once the prep work is completed, the actual release is straight forward: 32 | 33 | * First ensure that you have `release-it` installed globally, generally done by 34 | using one of the following commands: 35 | 36 | ``` 37 | # using https://volta.sh 38 | volta install release-it 39 | 40 | # using Yarn 41 | yarn global add release-it 42 | 43 | # using npm 44 | npm install --global release-it 45 | ``` 46 | 47 | * Second, ensure that you have installed your projects dependencies: 48 | 49 | ``` 50 | # using yarn 51 | yarn install 52 | 53 | # using npm 54 | npm install 55 | ``` 56 | 57 | * And last (but not least 😁) do your release: 58 | 59 | ``` 60 | release-it 61 | ``` 62 | 63 | [release-it](https://github.com/release-it/release-it/) manages the actual 64 | release process. It will prompt you through the process of choosing the version 65 | number, tagging, pushing the tag and commits, etc. 66 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | This guide will help you navigate any breaking changes in the API. 3 | 4 | ## 1.0.x and higher 5 | With version 1.0.0, breaking changes were introduced to the API. These 6 | changes were made to make the API methods more understandable. 7 | 8 | ### `canUnload` 9 | * `canUnload` was renamed to `isPageDirty`. Please rename any uses of `canUnload` to `isPageDirty`. 10 | * The default behavior of `canUnload` has changed. Previously, it looked for `controller.isDirty` by default. 11 | Because controllers will be removed in a future version of Ember, this is no longer 12 | the behavior. By default, we will get the current model for the Route and check 13 | if that model `hasDirtyAttributes`. You can maintain the existing default behavior 14 | like this: 15 | ```javascript 16 | export default Ember.Route.extend({ 17 | isPageDirty() { 18 | return !!get(this, 'controller.isDirty'); 19 | } 20 | }); 21 | ``` 22 | 23 | ### `allowUnload` 24 | * `allowUnload` was rename to `shouldCheckIsPageDirty`. Please rename any uses of 25 | `allowUnload` to `shouldCheckIsPageDirty`. 26 | * The default behavior remains the same. 27 | -------------------------------------------------------------------------------- /addon/mixins/confirmation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable ember/no-new-mixins */ 2 | 3 | import { get } from '@ember/object'; 4 | import { on } from '@ember/object/evented'; 5 | import Mixin from '@ember/object/mixin'; 6 | import Route from '@ember/routing/route'; 7 | 8 | export default Mixin.create({ 9 | _ensureConfirmationMixinOnRoute: on('init', function() { 10 | if (!(this instanceof Route)) { 11 | throw Error('ember-onbeforeunload ConfirmationMixin must be mixed into a Route.'); 12 | } 13 | }), 14 | 15 | confirmationMessage(/* model */) { 16 | return 'Unsaved changes! Are you sure you would like to continue?'; 17 | }, 18 | 19 | onUnload() { 20 | /* intentionally left blank to implement own custom teardown logic */ 21 | }, 22 | 23 | onBeforeunload(e) { 24 | if (this.isPageDirty(this.modelFor(this.routeName))) { 25 | const confirmationMessage = this.readConfirmation(); 26 | e.returnValue = confirmationMessage; // Gecko and Trident 27 | return confirmationMessage; // Gecko and WebKit 28 | } 29 | }, 30 | 31 | isPageDirty(model) { 32 | if (model) { 33 | return !!get(model, 'hasDirtyAttributes'); 34 | } else { 35 | return false; 36 | } 37 | }, 38 | 39 | handleEvent(event) { 40 | let fnName = event.type.split(''); 41 | 42 | if (fnName.length) { 43 | fnName[0] = fnName[0].toUpperCase(); 44 | const fn = this['on' + fnName.join('')]; 45 | 46 | if (typeof fn === 'function') { 47 | fn.apply(this, arguments); 48 | } 49 | } 50 | }, 51 | 52 | activate() { 53 | const _super = this._super(...arguments); 54 | 55 | if (window && window.addEventListener) { 56 | window.addEventListener('beforeunload', this, false); 57 | window.addEventListener('unload', this, false); 58 | } 59 | 60 | return _super; 61 | }, 62 | 63 | deactivate() { 64 | const _super = this._super(...arguments); 65 | 66 | if (window && window.removeEventListener) { 67 | window.removeEventListener('beforeunload', this, false); 68 | window.removeEventListener('unload', this, false); 69 | } 70 | 71 | return _super; 72 | }, 73 | 74 | readConfirmation() { 75 | let msg = get(this, 'confirmationMessage'); 76 | 77 | if (typeof msg === 'function') { 78 | const currentModel = this.modelFor(this.routeName); 79 | msg = msg.call(this, currentModel); 80 | } 81 | 82 | return msg; 83 | }, 84 | 85 | shouldCheckIsPageDirty(transition) { 86 | return transition.targetName.indexOf(this.routeName + '.') === 0; 87 | }, 88 | 89 | actions: { 90 | willTransition(transition) { 91 | this._super(...arguments); 92 | 93 | const allow = this.shouldCheckIsPageDirty(transition); 94 | 95 | if (!allow && this.isPageDirty(this.modelFor(this.routeName))) { 96 | const msg = this.readConfirmation(); 97 | 98 | if (this.customConfirm) { 99 | if (this._transitionConfirmed) { 100 | this._transitionConfirmed = undefined; 101 | return true; 102 | } 103 | 104 | transition.abort(); 105 | this.customConfirm(transition).then(() => { 106 | this._transitionConfirmed = true; 107 | transition.retry(); 108 | }); 109 | } else if (window && window.confirm && !window.confirm(msg)) { 110 | transition.abort(); 111 | return false; 112 | } 113 | } 114 | 115 | return true; 116 | } 117 | } 118 | }); 119 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmit/ember-onbeforeunload/496fa7e7a5fff8ee8029d25777f77239a937ffc7/app/.gitkeep -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = async function() { 6 | return { 7 | useYarn: true, 8 | scenarios: [ 9 | { 10 | name: 'ember-lts-3.4', 11 | npm: { 12 | devDependencies: { 13 | 'ember-source': '~3.4.0' 14 | } 15 | } 16 | }, 17 | { 18 | name: 'ember-lts-3.8', 19 | npm: { 20 | devDependencies: { 21 | 'ember-source': '~3.8.0' 22 | } 23 | } 24 | }, 25 | { 26 | name: 'ember-release', 27 | npm: { 28 | devDependencies: { 29 | 'ember-source': await getChannelURL('release') 30 | } 31 | } 32 | }, 33 | { 34 | name: 'ember-beta', 35 | npm: { 36 | devDependencies: { 37 | 'ember-source': await getChannelURL('beta') 38 | } 39 | } 40 | }, 41 | { 42 | name: 'ember-canary', 43 | npm: { 44 | devDependencies: { 45 | 'ember-source': await getChannelURL('canary') 46 | } 47 | } 48 | }, 49 | // The default `.travis.yml` runs this scenario via `npm test`, 50 | // not via `ember try`. It's still included here so that running 51 | // `ember try:each` manually or from a customized CI config will run it 52 | // along with all the other scenarios. 53 | { 54 | name: 'ember-default', 55 | npm: { 56 | devDependencies: {} 57 | } 58 | }, 59 | { 60 | name: 'ember-default-with-jquery', 61 | env: { 62 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 63 | 'jquery-integration': true 64 | }) 65 | }, 66 | npm: { 67 | devDependencies: { 68 | '@ember/jquery': '^0.5.1' 69 | } 70 | } 71 | } 72 | ] 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: 'ember-onbeforeunload' 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-onbeforeunload", 3 | "version": "2.0.0", 4 | "description": "An addon to conditionally prompt the user when transitioning between routes or closing the browser.", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "build": "ember build", 11 | "lint:js": "eslint .", 12 | "start": "ember serve", 13 | "test": "ember test", 14 | "test:all": "ember try:each", 15 | "release": "release-it" 16 | }, 17 | "repository": "https://github.com/jasonmit/ember-onbeforeunload", 18 | "homepage": "https://github.com/jasonmit/ember-onbeforeunload", 19 | "bugs": "https://github.com/jasonmit/ember-onbeforeunload/issues", 20 | "engines": { 21 | "node": " 8.* || >= 10.*" 22 | }, 23 | "author": "Jason Mitchell ", 24 | "contributors": [ 25 | { 26 | "name": "Ben Limmer", 27 | "email": "hello@benlimmer.com" 28 | }, 29 | { 30 | "name": "Ryan Mark", 31 | "email": "rgmark@gmail.com" 32 | } 33 | ], 34 | "license": "MIT", 35 | "devDependencies": { 36 | "@ember/jquery": "^1.1.0", 37 | "babel-eslint": "^10.0.3", 38 | "broccoli-asset-rev": "^3.0.0", 39 | "chai-dom": "^1.8.1", 40 | "ember-cli": "~3.13.1", 41 | "ember-cli-chai": "^0.5.0", 42 | "ember-cli-dependency-checker": "^3.2.0", 43 | "ember-cli-eslint": "^5.1.0", 44 | "ember-cli-inject-live-reload": "^2.0.2", 45 | "ember-cli-shims": "^1.2.0", 46 | "ember-cli-sri": "^2.1.1", 47 | "ember-cli-uglify": "^3.0.0", 48 | "ember-disable-prototype-extensions": "^1.1.3", 49 | "ember-export-application-global": "^2.0.0", 50 | "ember-load-initializers": "^2.1.1", 51 | "ember-maybe-import-regenerator": "^0.1.6", 52 | "ember-mocha": "^0.16.1", 53 | "ember-resolver": "^5.3.0", 54 | "ember-sinon": "~4.1.1", 55 | "ember-source": "~3.14.1", 56 | "ember-source-channel-url": "^2.0.1", 57 | "ember-try": "^1.2.1", 58 | "eslint-plugin-ember": "^7.3.0", 59 | "eslint-plugin-node": "^10.0.0", 60 | "loader.js": "^4.7.0", 61 | "release-it": "^12.4.3", 62 | "release-it-lerna-changelog": "^1.0.3" 63 | }, 64 | "keywords": [ 65 | "ember-addon" 66 | ], 67 | "dependencies": { 68 | "ember-cli-babel": "^7.12.0", 69 | "ember-cli-htmlbars": "^4.0.8" 70 | }, 71 | "ember-addon": { 72 | "configPath": "tests/dummy/config", 73 | "versionCompatibility": { 74 | "ember": ">=1.13.0" 75 | } 76 | }, 77 | "publishConfig": { 78 | "registry": "https://registry.npmjs.org" 79 | }, 80 | "release-it": { 81 | "plugins": { 82 | "release-it-lerna-changelog": { 83 | "infile": "CHANGELOG.md" 84 | } 85 | }, 86 | "git": { 87 | "tagName": "v${version}" 88 | }, 89 | "github": { 90 | "release": true 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | mode: 'ci', 13 | args: [ 14 | // --no-sandbox is needed when running Chrome inside a container 15 | process.env.CI ? '--no-sandbox' : null, 16 | '--headless', 17 | '--disable-dev-shm-usage', 18 | '--disable-software-rasterizer', 19 | '--mute-audio', 20 | '--remote-debugging-port=0', 21 | '--window-size=1440,900' 22 | ].filter(Boolean) 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | embertest: true 4 | }, 5 | globals: { 6 | "sandbox": true 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /tests/acceptance/smoke-test.js: -------------------------------------------------------------------------------- 1 | import { click, currentRouteName, visit } from '@ember/test-helpers'; 2 | import { 3 | describe, 4 | it, 5 | beforeEach, 6 | afterEach 7 | } from 'mocha'; 8 | import { expect } from 'chai'; 9 | import sinon from 'sinon' 10 | import startApp from '../helpers/start-app'; 11 | import destroyApp from '../helpers/destroy-app'; 12 | import { setupApplicationTest } from 'ember-mocha'; 13 | 14 | describe('Acceptance: Smoke Test', function() { 15 | setupApplicationTest(); 16 | let application, windowConfirmStub, sandbox; 17 | 18 | beforeEach(function() { 19 | application = startApp(); 20 | sandbox = sinon.createSandbox(); 21 | windowConfirmStub = sandbox.stub(window, 'confirm'); 22 | }); 23 | 24 | afterEach(function() { 25 | destroyApp(application); 26 | sandbox.restore(); 27 | }); 28 | 29 | it('does not confirm with the user if the record is not dirtied', async function() { 30 | await visit('/'); 31 | await click('#foo-link'); 32 | await click('#index-link'); 33 | expect(windowConfirmStub.called).to.be.false; 34 | }); 35 | 36 | it('confirms with the user if the record is dirtied', async function() { 37 | await visit('/'); 38 | await click('#foo-link'); 39 | await click('#dirty-record-button'); 40 | await click('#index-link'); 41 | expect(windowConfirmStub.calledOnce).to.be.true; 42 | }); 43 | 44 | it('it allows navigation if the user confirms', async function() { 45 | windowConfirmStub.returns(true); 46 | 47 | await visit('/'); 48 | await click('#foo-link'); 49 | await click('#dirty-record-button'); 50 | await click('#index-link'); 51 | expect(currentRouteName()).to.equal('index'); 52 | }); 53 | 54 | it('it aborts navigation if the user declines', async function() { 55 | windowConfirmStub.returns(false); 56 | 57 | await visit('/'); 58 | await click('#foo-link'); 59 | await click('#dirty-record-button'); 60 | await click('#index-link'); 61 | expect(currentRouteName()).to.equal('foo'); 62 | }); 63 | 64 | it('passes the model to the custom message', async function() { 65 | await visit('/'); 66 | await click('#foo-link'); 67 | await click('#dirty-record-button'); 68 | await click('#index-link'); 69 | const msg = windowConfirmStub.getCall(0).args[0]; 70 | expect(msg).to.contain('jasonmit'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmit/ember-onbeforeunload/496fa7e7a5fff8ee8029d25777f77239a937ffc7/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmit/ember-onbeforeunload/496fa7e7a5fff8ee8029d25777f77239a937ffc7/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmit/ember-onbeforeunload/496fa7e7a5fff8ee8029d25777f77239a937ffc7/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | this.route('index', { path: '/' }); 11 | this.route('foo'); 12 | }); 13 | 14 | export default Router; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/foo.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import Route from '@ember/routing/route'; 3 | import ConfirmationMixin from 'ember-onbeforeunload/mixins/confirmation'; 4 | 5 | export default Route.extend(ConfirmationMixin, { 6 | model() { 7 | return EmberObject.create({ 8 | id: 1, 9 | username: 'jasonmit', 10 | hasDirtyAttributes: false, 11 | }); 12 | }, 13 | confirmationMessage(model) { 14 | return `Unsaved changes for ${model.username}! Are you sure you want to continue?`; 15 | }, 16 | onUnload() { 17 | const model = this.modelFor(this.routeName); 18 | model.set('hasDirtyAttributes', false); 19 | }, 20 | actions: { 21 | markDirty() { 22 | const model = this.modelFor(this.routeName); 23 | model.set('hasDirtyAttributes', true); 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmit/ember-onbeforeunload/496fa7e7a5fff8ee8029d25777f77239a937ffc7/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

ember-onbeforeunload smoke test

2 | 3 | {{link-to 'index' 'index' id='index-link'}} 4 | {{link-to 'foo' 'foo' id='foo-link'}} 5 | 6 | {{outlet}} 7 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmit/ember-onbeforeunload/496fa7e7a5fff8ee8029d25777f77239a937ffc7/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/templates/foo.hbs: -------------------------------------------------------------------------------- 1 |

Foo

2 | 3 | 6 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |

Index

2 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmit/ember-onbeforeunload/496fa7e7a5fff8ee8029d25777f77239a937ffc7/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | 3 | export default function destroyApp(application) { 4 | run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Application from '../../app'; 2 | import config from '../../config/environment'; 3 | import { run } from '@ember/runloop'; 4 | 5 | export default function startApp(attrs) { 6 | let attributes = Object.assign({}, config.APP); 7 | attributes.autoboot = true; 8 | attributes = Object.assign(attributes, attrs); // use defaults, but you can override; 9 | 10 | return run(() => { 11 | let application = Application.create(attributes); 12 | application.setupForTesting(); 13 | application.injectTestHelpers(); 14 | return application; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-mocha'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmit/ember-onbeforeunload/496fa7e7a5fff8ee8029d25777f77239a937ffc7/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/mixins/confirmation-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable ember/no-new-mixins */ 2 | 3 | import EmberObject from '@ember/object'; 4 | import Route from '@ember/routing/route'; 5 | import { expect } from 'chai'; 6 | import { 7 | context, 8 | describe, 9 | it, 10 | beforeEach, 11 | afterEach 12 | } from 'mocha'; 13 | import ConfirmationMixin from 'ember-onbeforeunload/mixins/confirmation'; 14 | import sinon from 'sinon'; 15 | import { setupTest } from 'ember-mocha'; 16 | 17 | describe('ConfirmationMixin', function() { 18 | setupTest(); 19 | let defaultSubject, sandbox; 20 | beforeEach(function() { 21 | let ConfirmationRoute = Route.extend(ConfirmationMixin); 22 | defaultSubject = ConfirmationRoute.create(); 23 | sandbox = sinon.createSandbox(); 24 | }); 25 | 26 | afterEach(function() { 27 | sandbox.restore(); 28 | }); 29 | 30 | describe('init hook', function() { 31 | context('ensure mixed into Route', function() { 32 | it('allows mixing into a route', function() { 33 | expect(defaultSubject).to.be.ok; 34 | }); 35 | 36 | it('throws an exception when mixing into a plain-ol\' ember object', function() { 37 | let ConfirmationObject = EmberObject.extend(ConfirmationMixin); 38 | expect(ConfirmationObject.create.bind()).to.throw; 39 | }); 40 | }); 41 | }); 42 | 43 | describe('isPageDirty', function() { 44 | context('has model', function() { 45 | let subject, modelObj; 46 | beforeEach(function() { 47 | subject = defaultSubject; 48 | modelObj = EmberObject.create({ 49 | hasDirtyAttributes: undefined, 50 | }); 51 | }); 52 | 53 | it('returns hasDirtyAttributes from model', function() { 54 | modelObj.set('hasDirtyAttributes', true); 55 | expect(subject.isPageDirty(modelObj)).to.be.true; 56 | 57 | modelObj.set('hasDirtyAttributes', false); 58 | expect(subject.isPageDirty(modelObj)).to.be.false; 59 | }); 60 | }); 61 | 62 | context('no modelFor route', function() { 63 | let subject; 64 | beforeEach(function() { 65 | subject = defaultSubject; 66 | }); 67 | 68 | it('returns false', function() { 69 | expect(subject.isPageDirty()).to.be.false; 70 | }); 71 | }); 72 | }); 73 | 74 | describe('confirmationMessage', function() { 75 | it('defaults to a sane message', function() { 76 | const subject = defaultSubject; 77 | expect(subject.confirmationMessage()).to.include('Unsaved changes'); 78 | }); 79 | }); 80 | 81 | describe('readConfirmation', function() { 82 | it('handles overridden string confirmations', function() { 83 | const subject = Route.extend(ConfirmationMixin, { 84 | confirmationMessage() { 85 | return 'custom warning'; 86 | }, 87 | modelFor() { 88 | return null; 89 | }, 90 | }).create(); 91 | 92 | expect(subject.readConfirmation()).to.equal('custom warning'); 93 | }); 94 | 95 | it('calls overridden confirmation functions with the current route model', function() { 96 | const subject = Route.extend(ConfirmationMixin, { 97 | confirmationMessage(model) { 98 | return `custom warning with name: ${model.get('name')}`; 99 | }, 100 | routeName: 'test-route', 101 | }).create(); 102 | const modelForStub = sandbox.stub(subject, 'modelFor') 103 | .returns(EmberObject.create({ name: 'foo'})); 104 | 105 | expect(subject.readConfirmation()).to.equal('custom warning with name: foo'); 106 | expect(modelForStub.getCall(0).args).to.deep.equal([subject.routeName]); 107 | }); 108 | }); 109 | 110 | describe('shouldCheckIsPageDirty', function() { 111 | it('allows when navigating to child routes', function() { 112 | const parentRoute = 'parent.index'; 113 | const childRoute = `${parentRoute}.sub`; 114 | 115 | const subject = Route.extend(ConfirmationMixin, { 116 | routeName: parentRoute, 117 | }).create(); 118 | 119 | const transitionMock = { 120 | targetName: childRoute 121 | }; 122 | 123 | expect(subject.shouldCheckIsPageDirty(transitionMock)).to.be.true; 124 | }); 125 | 126 | it('does not allow when navigating to an unrelated route', function() { 127 | const parentRoute = 'parent.index'; 128 | const unrelatedRoute = `unrelated.index`; 129 | 130 | const subject = Route.extend(ConfirmationMixin, { 131 | routeName: parentRoute, 132 | }).create(); 133 | 134 | const transitionMock = { 135 | targetName: unrelatedRoute 136 | }; 137 | 138 | expect(subject.shouldCheckIsPageDirty(transitionMock)).to.be.false; 139 | }); 140 | }); 141 | 142 | describe('handleEvent', function() { 143 | context('beforeunload', function() { 144 | it('calls the onBeforeunload method with the event', function() { 145 | const subject = defaultSubject; 146 | const eventMock = { 147 | type: 'beforeunload', 148 | }; 149 | const onBeforeunloadStub = sandbox.stub(subject, 'onBeforeunload'); 150 | 151 | subject.handleEvent(eventMock); 152 | expect(onBeforeunloadStub.calledOnce).to.be.true; 153 | expect(onBeforeunloadStub.getCall(0).args).to.deep.equal([eventMock]); 154 | }); 155 | }); 156 | 157 | context('unload', function() { 158 | it('calls the onUnload method with the event', function() { 159 | const subject = defaultSubject; 160 | const eventMock = { 161 | type: 'unload', 162 | }; 163 | const onUnloadStub = sandbox.stub(subject, 'onUnload'); 164 | 165 | subject.handleEvent(eventMock); 166 | expect(onUnloadStub.calledOnce).to.be.true; 167 | expect(onUnloadStub.getCall(0).args).to.deep.equal([eventMock]); 168 | }); 169 | }); 170 | }); 171 | 172 | describe('onBeforeunload handler', function() { 173 | context('page is dirty', function() { 174 | let subject, eventMock; 175 | beforeEach(function() { 176 | subject = defaultSubject; 177 | sandbox.stub(subject, 'isPageDirty').returns(true); 178 | sandbox.stub(subject, 'readConfirmation').returns('Unsaved changes'); 179 | sandbox.stub(subject, 'modelFor').returns(undefined); 180 | 181 | eventMock = { 182 | type: 'beforeunload' 183 | }; 184 | }); 185 | 186 | it('sets the event returnValue to the confirmation message', function() { 187 | subject.onBeforeunload(eventMock); 188 | 189 | expect(eventMock.returnValue).to.equal('Unsaved changes'); 190 | }); 191 | 192 | it('returns the confirmation message from the handler', function() { 193 | const retVal = subject.onBeforeunload(eventMock); 194 | 195 | expect(retVal).to.equal('Unsaved changes'); 196 | }); 197 | }); 198 | 199 | context('page is not dirty', function() { 200 | let subject; 201 | beforeEach(function() { 202 | subject = defaultSubject; 203 | sandbox.stub(subject, 'isPageDirty').returns(false); 204 | sandbox.stub(subject, 'modelFor').returns(undefined); 205 | }); 206 | 207 | it('does not modify the event in any way', function() { 208 | const eventStub = sandbox.stub(); 209 | subject.onBeforeunload(eventStub); 210 | 211 | expect(eventStub.called).to.be.false; 212 | }); 213 | }); 214 | }); 215 | 216 | context('route lifecycle', function() { 217 | let addEventListenerStub, removeEventListenerStub; 218 | beforeEach(function() { 219 | addEventListenerStub = sandbox.stub(window, 'addEventListener'); 220 | removeEventListenerStub = sandbox.stub(window, 'removeEventListener'); 221 | }); 222 | 223 | describe('activate', function() { 224 | it('adds a listener for beforeunload event', function() { 225 | const subject = defaultSubject; 226 | subject.activate(); 227 | expect(addEventListenerStub.getCall(0).args).to.deep.equal(['beforeunload', subject, false]); 228 | }); 229 | 230 | it('adds a listener for unload event', function() { 231 | const subject = defaultSubject; 232 | subject.activate(); 233 | expect(addEventListenerStub.getCall(1).args).to.deep.equal(['unload', subject, false]); 234 | }); 235 | }); 236 | 237 | describe('deactivate', function() { 238 | it('removes the beforeunload listener', function() { 239 | const subject = defaultSubject; 240 | subject.deactivate(); 241 | expect(removeEventListenerStub.getCall(0).args).to.deep.equal(['beforeunload', subject, false]); 242 | }); 243 | 244 | it('removes the unload listener', function() { 245 | const subject = defaultSubject; 246 | subject.deactivate(); 247 | expect(removeEventListenerStub.getCall(1).args).to.deep.equal(['unload', subject, false]); 248 | }); 249 | }); 250 | 251 | describe('willTransition', function() { 252 | let windowConfirmStub; 253 | beforeEach(function() { 254 | windowConfirmStub = sandbox.stub(window, 'confirm'); 255 | }); 256 | 257 | context('dirty transition allowed', function() { 258 | let subject; 259 | beforeEach(function() { 260 | subject = defaultSubject; 261 | sandbox.stub(subject, 'shouldCheckIsPageDirty').returns(true); 262 | }); 263 | 264 | it('does not check isPageDirty', function() { 265 | const isPageDirtyStub = sandbox.stub(subject, 'isPageDirty'); 266 | 267 | subject.send('willTransition'); 268 | 269 | expect(isPageDirtyStub.called).to.be.false; 270 | }); 271 | 272 | it('does not confirm with the user even if page is dirty', function() { 273 | subject.send('willTransition'); 274 | 275 | expect(windowConfirmStub.called).to.be.false; 276 | }); 277 | }); 278 | 279 | context('dirty transition not allowed', function() { 280 | let subject; 281 | beforeEach(function() { 282 | subject = defaultSubject; 283 | sandbox.stub(subject, 'shouldCheckIsPageDirty').returns(false); 284 | sandbox.stub(subject, 'modelFor').returns(null); 285 | }); 286 | 287 | context('page is not dirty', function() { 288 | beforeEach(function() { 289 | sandbox.stub(subject, 'isPageDirty').returns(false); 290 | }); 291 | 292 | it('does not confirm with the user', function() { 293 | subject.send('willTransition'); 294 | 295 | expect(windowConfirmStub.called).to.be.false; 296 | }); 297 | 298 | it('bubbles the willTransition event', function() { 299 | const retVal = subject.send('willTransition'); 300 | expect(retVal).to.equal(true); 301 | }); 302 | }); 303 | 304 | context('page is dirty', function() { 305 | let transition, abortTransitionStub; 306 | beforeEach(function() { 307 | sandbox.stub(subject, 'isPageDirty').returns(true); 308 | 309 | abortTransitionStub = sandbox.stub(); 310 | transition = { 311 | abort: abortTransitionStub 312 | }; 313 | }); 314 | 315 | it('confirms the transition with the user', function() { 316 | subject.send('willTransition', transition); 317 | 318 | expect(windowConfirmStub.calledOnce).to.be.true; 319 | }); 320 | 321 | it('uses the result of readConfirmation for the message', function() { 322 | sandbox.stub(subject, 'readConfirmation').returns('my message'); 323 | subject.send('willTransition', transition); 324 | 325 | expect(windowConfirmStub.getCall(0).args).to.deep.equal(['my message']); 326 | }); 327 | 328 | it('bubbles the event if the user confirms with the confirm dialog', function() { 329 | windowConfirmStub.returns(true); 330 | const retVal = subject.send('willTransition', transition); 331 | 332 | expect(retVal).to.be.true; 333 | }); 334 | 335 | it('aborts the transition if the user rejects the confirm dialog', function() { 336 | windowConfirmStub.returns(false); 337 | subject.send('willTransition', transition); 338 | 339 | expect(abortTransitionStub.calledOnce).to.be.true; 340 | }); 341 | 342 | context('customConfirm', function() { 343 | let customConfirmStub, retryTransitionStub; 344 | beforeEach(function() { 345 | subject.customConfirm = customConfirmStub = sandbox.stub(); 346 | 347 | abortTransitionStub = sandbox.stub(); 348 | retryTransitionStub = sandbox.stub(); 349 | transition = { 350 | abort: abortTransitionStub, 351 | retry: retryTransitionStub, 352 | }; 353 | }); 354 | 355 | it('confirms the transition with the user and aborts', function(done) { 356 | let promise = Promise.reject(); 357 | promise.finally(() => { 358 | setTimeout(() => { 359 | expect(retryTransitionStub.calledOnce).to.be.false; 360 | done(); 361 | }, 0); 362 | }); 363 | 364 | customConfirmStub.returns(promise); 365 | subject.send('willTransition', transition); 366 | 367 | expect(customConfirmStub.calledOnce).to.be.true; 368 | expect(abortTransitionStub.calledOnce).to.be.true; 369 | }); 370 | 371 | it('confirms the transition with the user and retries', function(done) { 372 | let promise = Promise.resolve(); 373 | promise.finally(() => { 374 | setTimeout(() => { 375 | expect(retryTransitionStub.calledOnce).to.be.true; 376 | done(); 377 | }, 0); 378 | }); 379 | 380 | customConfirmStub.returns(promise); 381 | subject.send('willTransition', transition); 382 | 383 | expect(customConfirmStub.calledOnce).to.be.true; 384 | expect(abortTransitionStub.calledOnce).to.be.true; 385 | }); 386 | }); 387 | }); 388 | }); 389 | }); 390 | }); 391 | }); 392 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonmit/ember-onbeforeunload/496fa7e7a5fff8ee8029d25777f77239a937ffc7/vendor/.gitkeep --------------------------------------------------------------------------------