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 +
+
+

+

+ CircleCI + Codecov + npm version + npm version +

+

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 +

+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ Timing +
+ +
+
+ +
+
+ +
+ + +
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ © 2017 Leko +
+
+ + + 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'] + } + } + ] + } +}