diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..71bd11c
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["es2015"],
+ "plugins": ["transform-class-properties"]
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..6f0085f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,6 @@
+
+root = true
+
+[{*.js,*.json}]
+indent_style = space
+indent_size = 2
diff --git a/.gitignore b/.gitignore
index 5148e52..bec0dae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,5 @@ jspm_packages
# Optional REPL history
.node_repl_history
+/dist/
+!/dist/.keep
diff --git a/README.md b/README.md
index 2d3285d..8ac1a13 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,30 @@
-# in-progress.js
-Prevent screen transition if any state changed.
+
+
+ in-progress.js
+
+
+
+
+
+
+
+
+
+Prevent screen transition if any state changed.
## Getting started
```
+npm install in-progress
+bower install in-progress
```
## Usage
```js
-import InProgress, {
- NullDetector
- ObjectDetector
- FormValueDetector
- PromiseDetector
-} from 'in-progress'
-```
-
-### Change detectors
-#### NullDetector
+import { FormValueDetector } from 'in-progress'
-#### ObjectDetector
-
-#### FormValueDetector
+const formElement = document.querySelector('#some-form')
+const detector = new FormValueDetector(formElement)
+detector.observe()
+```
-#### PromiseDetector
+View [live demo](https://leko.github.io/in-progress.js/)
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..603e48c
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,23 @@
+{
+ "name": "in-progress",
+ "description": "Prevent screen transition if any state changed.",
+ "main": "dist/inprogress.min.js",
+ "authors": [
+ "Leko "
+ ],
+ "license": "MIT",
+ "keywords": [
+ "beforeunload",
+ "form",
+ "promise"
+ ],
+ "homepage": "https://github.com/Leko/in-progress.js",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "src",
+ "test",
+ "tests"
+ ]
+}
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 0000000..a4cf9c8
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,9 @@
+machine:
+ node:
+ version: 6.1.0
+test:
+ pre:
+ - npm run lint
+ post:
+ - npm run build
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/src/InProgress.js b/dist/.keep
similarity index 100%
rename from src/InProgress.js
rename to dist/.keep
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..3277359
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,149 @@
+
+
+
+
in-progress.js demo
+
+ Prevent screen transition if any state changed.
+ << Back to repository
+
+
+
+
+
+
+
+
+
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..1578493
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,29 @@
+const path = require('path')
+const gulp = require('gulp')
+const webpack = require('webpack-stream')
+const gutil = require('gulp-util')
+// const gzip = require('gulp-gzip')
+const rename = require('gulp-rename')
+const uglify = require('gulp-uglify')
+const sourcemaps = require('gulp-sourcemaps')
+const webpackConfig = require('./webpack.config.js')
+
+gulp.task('build', (done) => {
+ // inprogress.js
+ webpack(Object.assign(webpackConfig, {
+ entry: path.resolve(__dirname, 'index.js'),
+ output: {
+ filename: 'inprogress.js',
+ library: 'InProgress',
+ libraryTarget: 'umd'
+ }
+ }))
+ .pipe(gulp.dest('dist'))
+ // inprogress.min.js + source map
+ .pipe(sourcemaps.init())
+ .pipe(uglify().on('error', gutil.log))
+ // .pipe(gzip()) // FIXME: Unexpected token
+ .pipe(rename('inprogress.min.js'))
+ .pipe(sourcemaps.write('.'))
+ .pipe(gulp.dest('dist'))
+})
diff --git a/index.js b/index.js
index a643f46..01abcbc 100644
--- a/index.js
+++ b/index.js
@@ -1,13 +1,7 @@
-import InProgress from './src/InProgress'
import NullDetector from './src/NullDetector'
-import ObjectDetector from './src/ObjectDetector'
import FormValueDetector from './src/FormValueDetector'
-import PromiseDetector from './src/PromiseDetector'
-export default InProgress
export {
NullDetector,
- ObjectDetector,
- FormValueDetector,
- PromiseDetector
+ FormValueDetector
}
diff --git a/package.json b/package.json
index 8effe4f..546732f 100644
--- a/package.json
+++ b/package.json
@@ -2,9 +2,14 @@
"name": "in-progress",
"version": "0.8.0",
"description": "Prevent screen transition if any state changed.",
- "main": "index.js",
+ "main": "dist/inprogress.js",
+ "files": [
+ "dist/inprogress.js"
+ ],
"scripts": {
- "test": "mocha"
+ "build": "gulp build",
+ "lint": "standard --parser babel-eslint",
+ "test": "istanbul cover _mocha -- --require babel-register --recursive"
},
"repository": {
"type": "git",
@@ -20,5 +25,32 @@
"bugs": {
"url": "https://github.com/Leko/in-progress.js/issues"
},
- "homepage": "https://github.com/Leko/in-progress.js#readme"
+ "homepage": "https://github.com/Leko/in-progress.js#readme",
+ "dependencies": {
+ "deep-equal": "^1.0.1",
+ "form-serialize": "^0.7.1"
+ },
+ "devDependencies": {
+ "babel-core": "^6.23.1",
+ "babel-eslint": "^7.1.1",
+ "babel-loader": "^6.4.0",
+ "babel-plugin-transform-class-properties": "^6.23.0",
+ "babel-preset-env": "^1.2.1",
+ "babel-preset-es2015": "^6.22.0",
+ "babel-register": "^6.23.0",
+ "gulp": "^3.9.1",
+ "gulp-gzip": "^1.4.0",
+ "gulp-rename": "^1.2.2",
+ "gulp-sourcemaps": "^2.4.1",
+ "gulp-uglify": "^2.0.1",
+ "gulp-util": "^3.0.8",
+ "istanbul": "^1.1.0-alpha.1",
+ "jsdom": "^9.11.0",
+ "mocha": "^3.2.0",
+ "mocha-jsdom": "^1.1.0",
+ "pump": "^1.0.2",
+ "sinon": "^1.17.7",
+ "standard": "^9.0.1",
+ "webpack-stream": "^3.2.0"
+ }
}
diff --git a/src/FormValueDetector.js b/src/FormValueDetector.js
index e69de29..12d72e0 100644
--- a/src/FormValueDetector.js
+++ b/src/FormValueDetector.js
@@ -0,0 +1,22 @@
+/* eslint-env browser */
+import serialize from 'form-serialize'
+import NullDetector from './NullDetector'
+
+export default class FormValueDetector extends NullDetector {
+ static tags = [
+ 'input',
+ 'textarea',
+ 'select'
+ ]
+
+ constructor (formEl, options = {}) {
+ const elements = formEl instanceof NodeList ? Array.from(formEl) : [formEl]
+ super(() => this.getValues(elements))
+ }
+
+ getValues (formElements) {
+ return formElements.reduce((acc, formEl) =>
+ Object.assign(acc, { [formEl.name]: serialize(formEl, { hash: true, empty: true }) })
+ , {})
+ }
+}
diff --git a/src/NullDetector.js b/src/NullDetector.js
index e69de29..9c0b418 100644
--- a/src/NullDetector.js
+++ b/src/NullDetector.js
@@ -0,0 +1,43 @@
+
+import deepEqual from 'deep-equal'
+
+export default class NullDetector {
+ constructor (valueFn) {
+ this.valueFn = valueFn
+ this.initialValues = {}
+ this.handleBeforeUnload = this.handleBeforeUnload.bind(this)
+ }
+
+ handleBeforeUnload (e) {
+ if (this.inProgress()) {
+ e.returnValue = this.customMessage
+ e.preventDefault()
+ return this.customMessage
+ } else {
+ return null
+ }
+ }
+
+ observe (customMessage) {
+ this.reset()
+ this.customMessage = customMessage
+ window.addEventListener('beforeunload', this.handleBeforeUnload, false)
+ }
+
+ stopObserve () {
+ window.removeEventListener('beforeunload', this.handleBeforeUnload, false)
+ this.customMessage = null
+ }
+
+ reset () {
+ this.initialValues = this.valueFn()
+ }
+
+ inProgress () {
+ return this.hasChanges()
+ }
+
+ hasChanges () {
+ return !deepEqual(this.initialValues, this.valueFn())
+ }
+}
diff --git a/test/FormValueDetector.spec.js b/test/FormValueDetector.spec.js
new file mode 100644
index 0000000..2988067
--- /dev/null
+++ b/test/FormValueDetector.spec.js
@@ -0,0 +1,11 @@
+/* eslint-env mocha */
+import { FormValueDetector } from '../'
+
+describe(FormValueDetector.name, () => {
+ describe('#constructor', () => {
+
+ })
+ describe('#getValues', () => {
+
+ })
+})
diff --git a/test/NullDetector.spec.js b/test/NullDetector.spec.js
new file mode 100644
index 0000000..303af62
--- /dev/null
+++ b/test/NullDetector.spec.js
@@ -0,0 +1,102 @@
+/* eslint-env mocha */
+import assert from 'assert'
+import sinon from 'sinon'
+import jsdom from 'mocha-jsdom'
+import { NullDetector } from '../'
+
+describe(NullDetector.name, () => {
+ jsdom()
+
+ describe('#constructor', () => {
+ it('must have valueFn and initialValues', () => {
+ const detector = new NullDetector(() => {})
+ assert.ok(detector.valueFn)
+ assert.ok(detector.initialValues)
+ })
+ })
+ describe('#handleBeforeUnload', () => {
+ it('must return null if not in progress', () => {
+ const mockEvent = { returnValue: undefined, preventDefault: () => {} }
+ const detector = new NullDetector(() => {})
+ const stub = sinon.stub(detector, 'inProgress').returns(false)
+
+ assert.ok(!detector.handleBeforeUnload(mockEvent))
+
+ stub.restore()
+ })
+ it('must return string if in progress', () => {
+ const expected = 'xxx'
+ const mockEvent = { returnValue: undefined, preventDefault: () => {} }
+ const detector = new NullDetector(() => {})
+ const stub = sinon.stub(detector, 'inProgress').returns(true)
+ detector.customMessage = expected
+
+ assert.equal(detector.handleBeforeUnload(mockEvent), expected)
+
+ stub.restore()
+ })
+ })
+ describe('#observe', () => {
+ it('must add listener beforeunload event to this.handleBeforeUnload', () => {
+ const detector = new NullDetector(() => {})
+ const spy = sinon.spy(window, 'addEventListener')
+
+ detector.observe('xxx')
+
+ assert.ok(spy.calledWith('beforeunload', detector.handleBeforeUnload, false))
+ window.addEventListener.restore()
+ })
+ })
+ describe('#stopObserve', () => {
+ it('must remove listener beforeunload event to this.handleBeforeUnload', () => {
+ const detector = new NullDetector(() => {})
+ const spy = sinon.spy(window, 'removeEventListener')
+
+ detector.stopObserve()
+
+ assert.ok(spy.calledWith('beforeunload', detector.handleBeforeUnload, false))
+ window.removeEventListener.restore()
+ })
+ })
+ describe('#reset', () => {
+ it('must set initialValues', () => {
+ const expected = true
+ const detector = new NullDetector(() => expected)
+ detector.initialValues = null
+
+ detector.reset()
+
+ assert.equal(detector.initialValues, expected)
+ })
+ })
+ describe('#inProgress', () => {
+ it('must return false if not changes', () => {
+ const detector = new NullDetector(() => {})
+ const stub = sinon.stub(detector, 'hasChanges').returns(false)
+
+ assert.ok(!detector.inProgress())
+ stub.restore()
+ })
+ it('must return true if any changes', () => {
+ const detector = new NullDetector(() => {})
+ const stub = sinon.stub(detector, 'hasChanges').returns(true)
+
+ assert.ok(detector.inProgress())
+ stub.restore()
+ })
+ })
+ describe('#hasChanges', () => {
+ it('must return false if not changes', () => {
+ const detector = new NullDetector(() => ({ a: 1 }))
+ detector.initialValues = { a: 1 }
+
+ assert.ok(!detector.hasChanges())
+ })
+ it('must return true if any changes', () => {
+ const detector = new NullDetector(() => ({ a: 1 }))
+ detector.initialValues = { a: 2 }
+
+ assert.ok(detector.hasChanges())
+ })
+ })
+})
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..6a1bed0
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,15 @@
+module.exports = {
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ loader: 'babel-loader',
+ query: {
+ presets: ['env'],
+ plugins: ['transform-class-properties']
+ }
+ }
+ ]
+ }
+}