diff --git a/.babelrc b/.babelrc index facd18092..2aecabf0b 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,4 @@ { - "presets": ["es2015", "react"] + "presets": ["es2015", "react", "stage-0"], + "plugins": ["transform-decorators-legacy"], } \ No newline at end of file diff --git a/Makefile b/Makefile index 1b2dd5b22..510525937 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,18 @@ EXAMPLE_SRC = example/src STANDALONE = standalone SRC = src DIST = dist +TEST = test/*.test.js +MOCHA_OPTS = --compilers js:babel-core/register --require test/setup.js -b --timeout 20000 --reporter spec lint: - @echo linting... + @echo Linting... @$(NODE_BIN)/standard --verbose | $(NODE_BIN)/snazzy src/index.js -convertCss: +test: lint + @echo Start testing... + @$(NODE_BIN)/mocha $(MOCHA_OPTS) $(TEST) + +convertCSS: @echo Converting css... @node bin/transferSass.js @@ -24,7 +30,7 @@ dev: @echo starting dev server... @rm -rf $(EXAMPLE_DIST) @mkdir -p $(EXAMPLE_DIST) - @make convertCss + @make convertCSS @$(NODE_BIN)/watchify -t babelify $(EXAMPLE_SRC)/index.js -o $(EXAMPLE_DIST)/index.js -dv @$(NODE_BIN)/node-sass $(EXAMPLE_SRC)/index.scss $(EXAMPLE_DIST)/index.css @$(NODE_BIN)/node-sass -w $(EXAMPLE_SRC)/index.scss $(EXAMPLE_DIST)/index.css @@ -44,10 +50,10 @@ deployCSS: deploy: lint @echo Deploy... @rm -rf dist && mkdir dist - @make convertCss + @make convertCSS @make deployCSS @make deployJS @make genStand @echo success! -.PHONY: lint convertCss genStand dev deployJS deployCSS deploy +.PHONY: lint convertCSS genStand dev deployJS deployCSS deploy diff --git a/package.json b/package.json index ff7f23abd..98b1f8c01 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "react tooltip component", "main": "index.js", "scripts": { - "test": "make lint", + "test": "make test", "dev": "make start", "deploy": "make deploy" }, @@ -46,15 +46,26 @@ }, "devDependencies": { "babel-cli": "^6.5.1", + "babel-core": "^6.9.1", "babel-eslint": "^4.1.1", + "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-runtime": "^6.5.0", "babel-preset-es2015": "^6.5.0", "babel-preset-react": "^6.5.0", + "babel-preset-stage-0": "^6.5.0", "babelify": "^7.2.0", "browserify": "^13.0.0", "browserify-shim": "^3.8.12", + "chai": "^3.5.0", + "chai-enzyme": "^0.5.0", + "cheerio": "^0.20.0", + "enzyme": "^2.3.0", "http-server": "^0.8.0", + "jsdom": "^9.2.1", + "mocha": "^2.5.3", "node-sass": "^3.3.2", + "react-addons-test-utils": "^15.1.0", + "sinon": "^1.17.4", "snazzy": "^2.0.1", "standard": "^5.2.2", "tape": "^4.2.0", diff --git a/src/constant.js b/src/constant.js new file mode 100644 index 000000000..a5348fcf8 --- /dev/null +++ b/src/constant.js @@ -0,0 +1,7 @@ +export default { + + GLOBAL: { + HIDE: '__react_tooltip_hide_event', + REBUILD: '__react_tooltip_rebuild_event' + } +} diff --git a/src/decorators/staticMethods.js b/src/decorators/staticMethods.js new file mode 100644 index 000000000..835bed58f --- /dev/null +++ b/src/decorators/staticMethods.js @@ -0,0 +1,37 @@ +/** + * Static methods for react-tooltip + */ +import CONSTANT from '../constant' + +const dispatchGlobalEvent = (eventName) => { + // Compatibale with IE + // @see http://stackoverflow.com/questions/26596123/internet-explorer-9-10-11-event-constructor-doesnt-work + let event + + if (typeof window.Event === 'function') { + event = new window.Event(eventName) + } else { + event = document.createEvent('Event') + event.initEvent(eventName, false, true) + } + + window.dispatchEvent(event) +} + +export default function (target) { + /** + * Hide all tooltip + * @trigger ReactTooltip.hide() + */ + target.hide = () => { + dispatchGlobalEvent(CONSTANT.GLOBAL.HIDE) + } + + /** + * Rebuild all tooltip + * @trigger ReactTooltip.rebuild() + */ + target.rebuild = () => { + dispatchGlobalEvent(CONSTANT.GLOBAL.REBUILD) + } +} diff --git a/src/decorators/windowListener.js b/src/decorators/windowListener.js new file mode 100644 index 000000000..fbc62c6cc --- /dev/null +++ b/src/decorators/windowListener.js @@ -0,0 +1,26 @@ +/** + * Events that should be bound to the window + */ +import CONSTANT from '../constant' + +export default function (target) { + target.prototype.bindWindowEvents = function () { + // ReactTooltip.hide + window.removeEventListener(CONSTANT.GLOBAL.HIDE, this.globalHide) + window.addEventListener(CONSTANT.GLOBAL.HIDE, this.globalHide, false) + + // ReactTooltip.rebuild + window.removeEventListener(CONSTANT.GLOBAL.REBUILD, this.globalRebuild) + window.addEventListener(CONSTANT.GLOBAL.REBUILD, this.globalRebuild, false) + + // Resize + window.removeEventListener('resize', this.onWindowResize) + window.addEventListener('resize', this.onWindowResize, false) + } + + target.prototype.unbindWindowEvents = function () { + window.removeEventListener(CONSTANT.GLOBAL.HIDE, this.globalHide) + window.removeEventListener(CONSTANT.GLOBAL.REBUILD, this.globalRebuild) + window.removeEventListener('resize', this.onWindowResize) + } +} diff --git a/src/index.js b/src/index.js index c618e079a..6146ead83 100644 --- a/src/index.js +++ b/src/index.js @@ -3,36 +3,43 @@ import React, { Component, PropTypes } from 'react' import ReactDOM from 'react-dom' import classname from 'classnames' + +/* Decoraters */ +import staticMethods from './decorators/staticMethods' +import windowListener from './decorators/windowListener' + +/* CSS */ import cssStyle from './style' +@staticMethods @windowListener class ReactTooltip extends Component { /** * Class method * @see ReactTooltip.hide() && ReactTooltup.rebuild() */ - static hide () { - /** - * Check for ie - * @see http://stackoverflow.com/questions/26596123/internet-explorer-9-10-11-event-constructor-doesnt-work - */ - if (typeof window.Event === 'function') { - window.dispatchEvent(new window.Event('__react_tooltip_hide_event')) - } else { - let event = document.createEvent('Event') - event.initEvent('__react_tooltip_hide_event', false, true) - window.dispatchEvent(event) - } - } - - static rebuild () { - if (typeof window.Event === 'function') { - window.dispatchEvent(new window.Event('__react_tooltip_rebuild_event')) - } else { - let event = document.createEvent('Event') - event.initEvent('__react_tooltip_rebuild_event', false, true) - window.dispatchEvent(event) - } - } + // static hide () { + // /** + // * Check for ie + // * @see http://stackoverflow.com/questions/26596123/internet-explorer-9-10-11-event-constructor-doesnt-work + // */ + // if (typeof window.Event === 'function') { + // window.dispatchEvent(new window.Event('__react_tooltip_hide_event')) + // } else { + // let event = document.createEvent('Event') + // event.initEvent('__react_tooltip_hide_event', false, true) + // window.dispatchEvent(event) + // } + // } + + // static rebuild () { + // if (typeof window.Event === 'function') { + // window.dispatchEvent(new window.Event('__react_tooltip_rebuild_event')) + // } else { + // let event = document.createEvent('Event') + // event.initEvent('__react_tooltip_rebuild_event', false, true) + // window.dispatchEvent(event) + // } + // } globalHide () { if (this.mount) { @@ -83,15 +90,16 @@ class ReactTooltip extends Component { componentDidMount () { this.bindListener() this.setStyleHeader() - /* Add window event listener for hide and rebuild */ - window.removeEventListener('__react_tooltip_hide_event', this.globalHide) - window.addEventListener('__react_tooltip_hide_event', this.globalHide, false) - - window.removeEventListener('__react_tooltip_rebuild_event', this.globalRebuild) - window.addEventListener('__react_tooltip_rebuild_event', this.globalRebuild, false) - /* Add listener on window resize */ - window.removeEventListener('resize', this.onWindowResize) - window.addEventListener('resize', this.onWindowResize, false) + this.bindWindowEvents() + // /* Add window event listener for hide and rebuild */ + // window.removeEventListener('__react_tooltip_hide_event', this.globalHide) + // window.addEventListener('__react_tooltip_hide_event', this.globalHide, false) + + // window.removeEventListener('__react_tooltip_rebuild_event', this.globalRebuild) + // window.addEventListener('__react_tooltip_rebuild_event', this.globalRebuild, false) + // /* Add listener on window resize */ + // window.removeEventListener('resize', this.onWindowResize) + // window.addEventListener('resize', this.onWindowResize, false) } componentWillUpdate () { @@ -109,9 +117,10 @@ class ReactTooltip extends Component { this.unbindListener() this.removeScrollListener() this.mount = false - window.removeEventListener('__react_tooltip_hide_event', this.globalHide) - window.removeEventListener('__react_tooltip_rebuild_event', this.globalRebuild) - window.removeEventListener('resize', this.onWindowResize) + this.unbindWindowEvents() + // window.removeEventListener('__react_tooltip_hide_event', this.globalHide) + // window.removeEventListener('__react_tooltip_rebuild_event', this.globalRebuild) + // window.removeEventListener('resize', this.onWindowResize) } /* TODO: optimize, bind has been trigger too many times */ diff --git a/test/globalMethods.test.js b/test/globalMethods.test.js new file mode 100644 index 000000000..5f042f658 --- /dev/null +++ b/test/globalMethods.test.js @@ -0,0 +1,32 @@ +/* For Standard.js lint checking */ +/* eslint-env mocha */ + +import React from 'react' +import { mount } from 'enzyme' +import chai, { expect } from 'chai' +import chaiEnzyme from 'chai-enzyme' +import sinon from 'sinon' +import ReactTooltip from '../src' + +/* Initial test tools */ +chai.use(chaiEnzyme()) + +describe('Global methods', () => { + it('should be hided by invoking ReactTooltip.hide', () => { + const wrapper = mount() + wrapper.setState({ show: true }) + expect(wrapper).to.have.state('show', true) + ReactTooltip.hide() + setImmediate(() => { + expect(wrapper).to.have.state('show', false) + }) + }) + + it('should be rebuild by invoking ReactTooltip.rebuild', () => { + sinon.spy(ReactTooltip.prototype, 'globalRebuild') + ReactTooltip.rebuild() + setImmediate(() => { + expect(ReactTooltip.prototype.globalRebuild.calledOnce).to.equal(true) + }) + }) +}) diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 000000000..ccdca868b --- /dev/null +++ b/test/setup.js @@ -0,0 +1,20 @@ +/** + * Setup jsdom for enzyme mount + * @see https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md#using-enzyme-with-jsdom + */ +import {jsdom} from 'jsdom' + +const exposedProperties = ['window', 'navigator', 'document'] + +global.document = jsdom('') +global.window = document.defaultView +Object.keys(document.defaultView).forEach((property) => { + if (typeof global[property] === 'undefined') { + exposedProperties.push(property) + global[property] = document.defaultView[property] + } +}) + +global.navigator = { + userAgent: 'node.js' +}