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
new file mode 100644
index 000000000..284c57819
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,67 @@
+NODE_BIN = node_modules/.bin
+EXAMPLE_DIST = example/dist
+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...
+ @$(NODE_BIN)/standard --verbose | $(NODE_BIN)/snazzy src/index.js
+
+test: lint
+ @echo Start testing...
+ @$(NODE_BIN)/mocha $(MOCHA_OPTS) $(TEST)
+
+convertCSS:
+ @echo Converting css...
+ @node bin/transferSass.js
+
+genStand:
+ @echo Generating standard...
+ @rm -rf $(STANDALONE) && mkdir $(STANDALONE)
+ @$(NODE_BIN)/browserify -t babelify -t browserify-shim $(SRC)/index.js --standalone ReactTooltip -o $(STANDALONE)/react-tooltip.js
+ @$(NODE_BIN)/browserify -t babelify -t browserify-shim $(SRC)/index.js --standalone ReactTooltip | $(NODE_BIN)/uglifyjs > $(STANDALONE)/react-tooltip.min.js
+ @cp $(DIST)/style.js $(STANDALONE)/style.js
+
+devJS:
+ @$(NODE_BIN)/watchify -t babelify $(EXAMPLE_SRC)/index.js -o $(EXAMPLE_DIST)/index.js -dv
+
+devCSS:
+ @$(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
+
+devServer:
+ @echo Listening 8888...
+ @$(NODE_BIN)/http-server example -p 8888 -s
+
+dev:
+ @echo starting dev server...
+ @rm -rf $(EXAMPLE_DIST)
+ @mkdir -p $(EXAMPLE_DIST)
+ @make convertCSS
+ @$(NODE_BIN)/concurrently --kill-others "make devJS" "make devCSS" "make devServer"
+
+deployJS:
+ @echo Generating deploy JS files...
+ @$(NODE_BIN)/babel $(SRC)/index.js -o $(DIST)/react-tooltip.js
+ @$(NODE_BIN)/babel $(SRC)/style.js -o $(DIST)/style.js
+ @$(NODE_BIN)/babel $(SRC)/index.js | $(NODE_BIN)/uglifyjs > $(DIST)/react-tooltip.min.js
+
+deployCSS:
+ @echo Generating deploy CSS files...
+ @cp $(SRC)/index.scss $(DIST)/react-tooltip.scss
+ @$(NODE_BIN)/node-sass --output-style compressed $(SRC)/index.scss $(DIST)/react-tooltip.min.css
+
+deploy: lint
+ @echo Deploy...
+ @rm -rf dist && mkdir dist
+ @make convertCSS
+ @make deployCSS
+ @make deployJS
+ @make genStand
+ @echo success!
+
+.PHONY: lint convertCSS genStand devJS devCSS devServer dev deployJS deployCSS deploy
diff --git a/bin/transferSass.js b/bin/transferSass.js
index 9809a9c23..6f82e7e11 100644
--- a/bin/transferSass.js
+++ b/bin/transferSass.js
@@ -16,6 +16,7 @@ function transferSass () {
console.error(err)
}
console.log('css file has been transformed successful')
+ process.exit()
})
})
}
diff --git a/circle.yml b/circle.yml
index 3703a7b0c..40cd00b9c 100644
--- a/circle.yml
+++ b/circle.yml
@@ -1,6 +1,6 @@
machine:
node:
- version: 0.12.0
+ version: 4.2.1
dependencies:
override:
diff --git a/example/src/index.js b/example/src/index.js
index b3aa94485..c3842f401 100755
--- a/example/src/index.js
+++ b/example/src/index.js
@@ -2,7 +2,7 @@
import React from 'react'
import {render} from 'react-dom'
-import ReactTooltip from '../../src/index'
+import ReactTooltip from '../../src'
const Test = React.createClass({
@@ -152,18 +152,75 @@ const Test = React.createClass({
+
+
+
+
Theme and delay
+
+
+
+
+
+
+
{"(・ω´・ )́)\n" +
+ "\n" +
+ ".extraClass {\n" +
+ " font-size: 20px !important;\n" +
+ " pointer-events: auto !important;\n" +
+ " &:hover {\n" +
+ "visibility: visible !important;\n" +
+ "opacity: 1 !important;\n" +
+ " }\n" +
+ "}"}
+
+
+
+
{"(・ω´・ )́)\n" +
+ "\n" +
+ " .customeTheme {\n" +
+ " color: #ff6e00 !important;\n" +
+ " background-color: orange !important;\n" +
+ " &.place-top {\n" +
+ " &:after {\n" +
+ " border-top-color: orange !important;\n" +
+ " border-top-style: solid !important;\n" +
+ " border-top-width: 6px !important;\n" +
+ " }\n" +
+ " }\n" +
+ "}"}
+
+
+
)
diff --git a/example/src/index.scss b/example/src/index.scss
index 43b72fe37..7af637d32 100755
--- a/example/src/index.scss
+++ b/example/src/index.scss
@@ -206,3 +206,25 @@ html, body{
line-height: 30px;
}
}
+
+// Extra class for demonstration
+.extraClass {
+ font-size: 20px !important;
+ pointer-events: auto !important;
+ &:hover {
+ visibility: visible !important;
+ opacity: 1 !important;
+ }
+}
+
+.customeTheme {
+ color: #ff6e00 !important;
+ background-color: orange !important;
+ &.place-top {
+ &:after {
+ border-top-color: orange !important;
+ border-top-style: solid !important;
+ border-top-width: 6px !important;
+ }
+ }
+}
diff --git a/package.json b/package.json
index 3d7a1db9d..5771218fe 100644
--- a/package.json
+++ b/package.json
@@ -1,25 +1,12 @@
{
"name": "react-tooltip",
- "version": "2.0.3",
+ "version": "3.0.0",
"description": "react tooltip component",
"main": "index.js",
"scripts": {
- "check": "standard --verbose | snazzy src/index.js",
- "test": "npm run check",
- "devjs": "node bin/transferSass.js & watchify -t babelify ./example/src/index.js -o ./example/dist/index.js -dv",
- "devcss": "node-sass example/src/index.scss example/dist/index.css & node-sass -w example/src/index.scss example/dist/index.css",
- "predev": "rm -rf example/dist",
- "dev": "mkdir -p ./example/dist & npm run devjs & npm run devcss & http-server example -p 8888 -s -o",
- "deployjs": "babel src/index.js -o dist/react-tooltip.js && babel src/style.js -o dist/style.js && npm run deployminjs",
- "deployminjs": "babel src/index.js | uglifyjs > dist/react-tooltip.min.js",
- "predeploycss": "cp src/index.scss dist/react-tooltip.scss",
- "deploycss": "node-sass --output-style compressed src/index.scss dist/react-tooltip.min.css",
- "predeploy": "rm -rf dist",
- "deploy": "mkdir dist & npm run deployjs & npm run deploycss & npm run standjs",
- "prestandjs": "rm -rf standalone",
- "standjs": "mkdir standalone && browserify -t babelify -t browserify-shim src/index.js --standalone ReactTooltip -o standalone/react-tooltip.js & npm run standminjs",
- "standminjs": "browserify -t babelify -t browserify-shim src/index.js --standalone ReactTooltip | uglifyjs > standalone/react-tooltip.min.js",
- "poststandjs": "cp ./dist/style.js ./standalone/style.js"
+ "test": "make test",
+ "dev": "make start",
+ "deploy": "make deploy"
},
"standard": {
"parser": "babel-eslint",
@@ -59,15 +46,27 @@
},
"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",
+ "concurrently": "^2.1.0",
+ "enzyme": "^2.3.0",
"http-server": "^0.8.0",
- "node-sass": "^3.3.2",
+ "jsdom": "^9.2.1",
+ "mocha": "^2.5.3",
+ "node-sass": "^3.7.0",
+ "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..f6d070f3f
--- /dev/null
+++ b/src/constant.js
@@ -0,0 +1,8 @@
+export default {
+
+ GLOBAL: {
+ HIDE: '__react_tooltip_hide_event',
+ REBUILD: '__react_tooltip_rebuild_event',
+ SHOW: '__react_tooltip_show_event'
+ }
+}
diff --git a/src/decorators/customEvent.js b/src/decorators/customEvent.js
new file mode 100644
index 000000000..fd4627004
--- /dev/null
+++ b/src/decorators/customEvent.js
@@ -0,0 +1,68 @@
+/**
+ * Custom events to control showing and hiding of tooltip
+ *
+ * @attributes
+ * - `event` {String}
+ * - `eventOff` {String}
+ */
+
+const checkStatus = function (dataEventOff, e) {
+ const {show} = this.state
+ const dataIsCapture = e.currentTarget.getAttribute('data-iscapture')
+ const isCapture = dataIsCapture && dataIsCapture === 'true' || this.props.isCapture
+ const currentItem = e.currentTarget.getAttribute('currentItem')
+
+ if (!isCapture) e.stopPropagation()
+ if (show && currentItem === 'true') {
+ if (!dataEventOff) this.hideTooltip(e)
+ } else {
+ e.currentTarget.setAttribute('currentItem', 'true')
+ setUntargetItems(e.currentTarget, this.getTargetArray())
+ this.showTooltip(e)
+ }
+}
+
+const setUntargetItems = function (currentTarget, targetArray) {
+ for (let i = 0; i < targetArray.length; i++) {
+ if (currentTarget !== targetArray[i]) {
+ targetArray[i].setAttribute('currentItem', 'false')
+ } else {
+ targetArray[i].setAttribute('currentItem', 'true')
+ }
+ }
+}
+
+export default function (target) {
+ target.prototype.isCustomEvent = function (ele) {
+ const {event} = this.state
+ return event || ele.getAttribute('data-event')
+ }
+
+ /* Bind listener for custom event */
+ target.prototype.customBindListener = function (ele) {
+ const {event, eventOff} = this.state
+ const dataEvent = ele.getAttribute('data-event') || event
+ const dataEventOff = ele.getAttribute('data-event-off') || eventOff
+
+ dataEvent.split(' ').forEach(event => {
+ ele.removeEventListener(event, checkStatus)
+ ele.addEventListener(event, checkStatus.bind(this, dataEventOff), false)
+ })
+ if (dataEventOff) {
+ dataEventOff.split(' ').forEach(event => {
+ ele.removeEventListener(event, this.hideTooltip)
+ ele.addEventListener(event, ::this.hideTooltip, false)
+ })
+ }
+ }
+
+ /* Unbind listener for custom event */
+ target.prototype.customUnbindListener = function (ele) {
+ const {event, eventOff} = this.state
+ const dataEvent = event || ele.getAttribute('data-event')
+ const dataEventOff = eventOff || ele.getAttribute('data-event-off')
+
+ ele.removeEventListener(dataEvent, checkStatus)
+ if (dataEventOff) ele.removeEventListener(dataEventOff, this.hideTooltip)
+ }
+}
diff --git a/src/decorators/isCapture.js b/src/decorators/isCapture.js
new file mode 100644
index 000000000..5701dac3a
--- /dev/null
+++ b/src/decorators/isCapture.js
@@ -0,0 +1,10 @@
+/**
+ * Util method to judge if it should follow capture model
+ */
+
+export default function (target) {
+ target.prototype.isCapture = function (currentTarget) {
+ const dataIsCapture = currentTarget.getAttribute('data-iscapture')
+ return dataIsCapture && dataIsCapture === 'true' || this.props.isCapture || false
+ }
+}
diff --git a/src/decorators/staticMethods.js b/src/decorators/staticMethods.js
new file mode 100644
index 000000000..ebc60a59b
--- /dev/null
+++ b/src/decorators/staticMethods.js
@@ -0,0 +1,44 @@
+/**
+ * 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)
+ }
+
+ target.prototype.globalRebuild = function () {
+ if (this.mount) {
+ this.unbindListener()
+ this.bindListener()
+ }
+ }
+}
diff --git a/src/decorators/windowListener.js b/src/decorators/windowListener.js
new file mode 100644
index 000000000..3f5dcf8c2
--- /dev/null
+++ b/src/decorators/windowListener.js
@@ -0,0 +1,35 @@
+/**
+ * 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.hideTooltip)
+ window.addEventListener(CONSTANT.GLOBAL.HIDE, ::this.hideTooltip, 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.hideTooltip)
+ window.removeEventListener(CONSTANT.GLOBAL.REBUILD, this.globalRebuild)
+ window.removeEventListener(CONSTANT.GLOBAL.REBUILD, this.globalShow)
+ window.removeEventListener('resize', this.onWindowResize)
+ }
+
+ /**
+ * invoked by resize event of window
+ */
+ target.prototype.onWindowResize = function () {
+ if (!this.mount) return
+ this.hideTooltip()
+ }
+}
diff --git a/src/index.js b/src/index.js
index c618e079a..9eef00a93 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,65 +3,52 @@
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import classname from 'classnames'
-import cssStyle from './style'
-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)
- }
- }
+/* Decoraters */
+import staticMethods from './decorators/staticMethods'
+import windowListener from './decorators/windowListener'
+import customEvent from './decorators/customEvent'
+import isCapture from './decorators/isCapture'
- 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)
- }
- }
+/* Utils */
+import getPosition from './utils/getPosition'
+import getTipContent from './utils/getTipContent'
- globalHide () {
- if (this.mount) {
- this.hideTooltip()
- }
- }
+/* CSS */
+import cssStyle from './style'
- globalRebuild () {
- if (this.mount) {
- this.unbindListener()
- this.bindListener()
- }
+@staticMethods @windowListener @customEvent @isCapture
+class ReactTooltip extends Component {
+
+ static propTypes = {
+ children: PropTypes.any,
+ place: PropTypes.string,
+ type: PropTypes.string,
+ effect: PropTypes.string,
+ offset: PropTypes.object,
+ multiline: PropTypes.bool,
+ border: PropTypes.bool,
+ class: PropTypes.string,
+ id: PropTypes.string,
+ html: PropTypes.bool,
+ delayHide: PropTypes.number,
+ delayShow: PropTypes.number,
+ event: PropTypes.string,
+ eventOff: PropTypes.string,
+ watchWindow: PropTypes.bool,
+ isCapture: PropTypes.bool,
+ globalEventOff: PropTypes.string
}
constructor (props) {
super(props)
- this._bind('showTooltip', 'updateTooltip', 'hideTooltip', 'checkStatus', 'onWindowResize', 'bindClickListener', 'globalHide', 'globalRebuild')
- this.mount = true
this.state = {
+ place: 'top', // Direction of tooltip
+ type: 'dark', // Color theme of tooltip
+ effect: 'float', // float or fixed
show: false,
border: false,
- multilineCount: 0,
placeholder: '',
- x: 'NONE',
- y: 'NONE',
- place: '',
- type: '',
- effect: '',
- multiline: false,
offset: {},
extraClass: '',
html: false,
@@ -69,209 +56,122 @@ class ReactTooltip extends Component {
delayShow: 0,
event: props.event || null,
eventOff: props.eventOff || null,
- isCapture: props.isCapture || false
+ currentEvent: null, // Current mouse event
+ currentTarget: null // Current target of mouse event
}
+
+ this.mount = true
this.delayShowLoop = null
this.delayHideLoop = null
}
- /* Bind this with method */
- _bind (...handlers) {
- handlers.forEach(handler => this[handler] = this[handler].bind(this))
- }
-
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)
- }
-
- componentWillUpdate () {
- this.unbindListener()
- }
-
- componentDidUpdate () {
- this.updatePosition()
- this.bindListener()
+ this.setStyleHeader() // Set the style to the
+ this.bindListener() // Bind listener for tooltip
+ this.bindWindowEvents() // Bind global event for static method
}
componentWillUnmount () {
+ this.mount = false
+
clearTimeout(this.delayShowLoop)
clearTimeout(this.delayHideLoop)
+
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)
- }
-
- /* TODO: optimize, bind has been trigger too many times */
- bindListener () {
- let targetArray = this.getTargetArray()
-
- let dataEvent
- let dataEventOff
- for (let i = 0; i < targetArray.length; i++) {
- if (targetArray[i].getAttribute('currentItem') === null) {
- targetArray[i].setAttribute('currentItem', 'false')
- }
- dataEvent = this.state.event || targetArray[i].getAttribute('data-event')
- if (dataEvent) {
- // if off event is specified, we will show tip on data-event and hide it on data-event-off
- dataEventOff = this.state.eventOff || targetArray[i].getAttribute('data-event-off')
-
- targetArray[i].removeEventListener(dataEvent, this.checkStatus)
- targetArray[i].addEventListener(dataEvent, this.checkStatus, false)
- if (dataEventOff) {
- targetArray[i].removeEventListener(dataEventOff, this.hideTooltip)
- targetArray[i].addEventListener(dataEventOff, this.hideTooltip, false)
- }
- } else {
- targetArray[i].removeEventListener('mouseenter', this.showTooltip)
- targetArray[i].addEventListener('mouseenter', this.showTooltip, false)
-
- if (this.state.effect === 'float') {
- targetArray[i].removeEventListener('mousemove', this.updateTooltip)
- targetArray[i].addEventListener('mousemove', this.updateTooltip, false)
- }
-
- targetArray[i].removeEventListener('mouseleave', this.hideTooltip)
- targetArray[i].addEventListener('mouseleave', this.hideTooltip, false)
- }
- }
- }
-
- unbindListener () {
- let targetArray = document.querySelectorAll('[data-tip]')
- let dataEvent
-
- for (let i = 0; i < targetArray.length; i++) {
- dataEvent = this.state.event || targetArray[i].getAttribute('data-event')
- if (dataEvent) {
- targetArray[i].removeEventListener(dataEvent, this.checkStatus)
- } else {
- targetArray[i].removeEventListener('mouseenter', this.showTooltip)
- targetArray[i].removeEventListener('mousemove', this.updateTooltip)
- targetArray[i].removeEventListener('mouseleave', this.hideTooltip)
- }
- }
+ this.unbindWindowEvents()
}
/**
- * Get all tooltip targets
+ * Pick out corresponded target elements
*/
- getTargetArray () {
- const {id} = this.props
+ getTargetArray (id) {
let targetArray
- if (id === undefined) {
+ if (!id) {
targetArray = document.querySelectorAll('[data-tip]:not([data-for])')
} else {
- targetArray = document.querySelectorAll('[data-tip][data-for="' + id + '"]')
+ targetArray = document.querySelectorAll(`[data-tip][data-for=${id}]`)
}
- return targetArray
+ // targetArray is a NodeList, convert it to a real array
+ // I hope I can use Object.values...
+ return Object.keys(targetArray).filter(key => key !== 'length').map(key => {
+ return targetArray[key]
+ })
}
/**
- * listener on window resize
+ * Bind listener to the target elements
+ * These listeners used to trigger showing or hiding the tooltip
*/
- onWindowResize () {
- if (!this.mount) return
- let targetArray = this.getTargetArray()
-
- for (let i = 0; i < targetArray.length; i++) {
- if (targetArray[i].getAttribute('currentItem') === 'true') {
- // todo: timer for performance
- let {x, y} = this.getPosition(targetArray[i])
- ReactDOM.findDOMNode(this).style.left = x + 'px'
- ReactDOM.findDOMNode(this).style.top = y + 'px'
- /* this.setState({
- x,
- y
- }) */
+ bindListener () {
+ const {id, globalEventOff} = this.props
+ let targetArray = this.getTargetArray(id)
+
+ targetArray.forEach(target => {
+ const isCaptureMode = this.isCapture(target)
+ if (target.getAttribute('currentItem') === null) {
+ target.setAttribute('currentItem', 'false')
}
- }
- }
- /**
- * Used in customer event
- */
- checkStatus (e) {
- const {show} = this.state
- let isCapture
+ if (this.isCustomEvent(target)) {
+ this.customBindListener(target)
+ return
+ }
- if (e.currentTarget.getAttribute('data-iscapture')) {
- isCapture = e.currentTarget.getAttribute('data-iscapture') === 'true'
- } else {
- isCapture = this.state.isCapture
- }
+ target.removeEventListener('mouseenter', this.showTooltip)
+ target.addEventListener('mouseenter', ::this.showTooltip, isCaptureMode)
+ if (this.state.effect === 'float') {
+ target.removeEventListener('mousemove', this.updateTooltip)
+ target.addEventListener('mousemove', ::this.updateTooltip, isCaptureMode)
+ }
- if (!isCapture) e.stopPropagation()
- if (show && e.currentTarget.getAttribute('currentItem') === 'true') {
- this.hideTooltip(e)
- } else {
- e.currentTarget.setAttribute('currentItem', 'true')
- /* when click other place, the tooltip should be removed */
- window.removeEventListener('click', this.bindClickListener)
- window.addEventListener('click', this.bindClickListener, isCapture)
+ target.removeEventListener('mouseleave', this.hideTooltip)
+ target.addEventListener('mouseleave', ::this.hideTooltip, isCaptureMode)
+ })
- this.showTooltip(e)
- this.setUntargetItems(e.currentTarget)
+ // Global event to hide tooltip
+ if (globalEventOff) {
+ window.removeEventListener(globalEventOff, this.hideTooltip)
+ window.addEventListener(globalEventOff, ::this.hideTooltip, false)
}
}
- setUntargetItems (currentTarget) {
- let targetArray = this.getTargetArray()
- for (let i = 0; i < targetArray.length; i++) {
- if (currentTarget !== targetArray[i]) {
- targetArray[i].setAttribute('currentItem', 'false')
- } else {
- targetArray[i].setAttribute('currentItem', 'true')
+ /**
+ * Unbind listeners on target elements
+ */
+ unbindListener () {
+ const {id, globalEventOff} = this.props
+ const targetArray = this.getTargetArray(id)
+
+ targetArray.forEach(target => {
+ if (this.isCustomEvent(target)) {
+ this.customUnbindListener(target)
+ return
}
- }
- }
- bindClickListener () {
- this.globalHide()
- window.removeEventListener('click', this.bindClickListener)
+ target.removeEventListener('mouseenter', this.showTooltip)
+ target.removeEventListener('mousemove', this.updateTooltip)
+ target.removeEventListener('mouseleave', this.hideTooltip)
+ })
+
+ if (globalEventOff) window.removeEventListener(globalEventOff, this.hideTooltip)
}
/**
- * When mouse enter, show update
+ * When mouse enter, show the tooltip
*/
showTooltip (e) {
+ // Get the tooltip content
+ // calculate in this phrase so that tip width height can be detected
+ const {children, multiline} = this.props
const originTooltip = e.currentTarget.getAttribute('data-tip')
- /* Detect multiline */
- const regexp = /
/
- const multiline = e.currentTarget.getAttribute('data-multiline') ? e.currentTarget.getAttribute('data-multiline') : (this.props.multiline ? this.props.multiline : false)
- let tooltipText
- let multilineCount = 0
- if (!multiline || multiline === 'false' || !regexp.test(originTooltip)) {
- tooltipText = originTooltip
- } else {
- tooltipText = originTooltip.split(regexp).map((d, i) => {
- multilineCount += 1
- return (
- {d}
- )
- })
- }
- /* Define extra class */
- let extraClass = e.currentTarget.getAttribute('data-class') ? e.currentTarget.getAttribute('data-class') : ''
- extraClass = this.props.class ? this.props.class + ' ' + extraClass : extraClass
+ const isMultiline = e.currentTarget.getAttribute('data-multiline') || multiline || false
+ const placeholder = getTipContent(originTooltip, children, isMultiline)
+
this.setState({
- placeholder: tooltipText,
- multilineCount: multilineCount,
+ placeholder,
place: e.currentTarget.getAttribute('data-place') || this.props.place || 'top',
type: e.currentTarget.getAttribute('data-type') || this.props.type || 'dark',
effect: e.currentTarget.getAttribute('data-effect') || this.props.effect || 'float',
@@ -280,12 +180,11 @@ class ReactTooltip extends Component {
delayShow: e.currentTarget.getAttribute('data-delay-show') || this.props.delayShow || 0,
delayHide: e.currentTarget.getAttribute('data-delay-hide') || this.props.delayHide || 0,
border: e.currentTarget.getAttribute('data-border') === 'true' || this.props.border || false,
- extraClass,
- multiline
+ extraClass: e.currentTarget.getAttribute('data-class') || this.props.class || ''
+ }, () => {
+ this.addScrollListener(e)
+ this.updateTooltip(e)
})
-
- this.addScrollListener()
- this.updateTooltip(e)
}
/**
@@ -293,26 +192,21 @@ class ReactTooltip extends Component {
*/
updateTooltip (e) {
const {delayShow, show} = this.state
+ let {placeholder} = this.state
const delayTime = show ? 0 : parseInt(delayShow, 10)
const eventTarget = e.currentTarget
clearTimeout(this.delayShowLoop)
this.delayShowLoop = setTimeout(() => {
- if (this.trim(this.state.placeholder).length > 0) {
- if (this.state.effect === 'float') {
- this.setState({
- show: true,
- x: e.clientX,
- y: e.clientY
- })
- } else if (this.state.effect === 'solid') {
- let {x, y} = this.getPosition(eventTarget)
- this.setState({
- show: true,
- x,
- y
- })
- }
+ if (typeof placeholder === 'string') placeholder = placeholder.trim()
+ if (Array.isArray(placeholder) && placeholder.length > 0 || placeholder) {
+ this.setState({
+ currentEvent: e,
+ currentTarget: eventTarget,
+ show: true
+ }, () => {
+ this.updatePosition()
+ })
}
}, delayTime)
}
@@ -322,6 +216,9 @@ class ReactTooltip extends Component {
*/
hideTooltip () {
const {delayHide} = this.state
+
+ if (!this.mount) return
+
clearTimeout(this.delayShowLoop)
clearTimeout(this.delayHideLoop)
this.delayHideLoop = setTimeout(() => {
@@ -334,265 +231,37 @@ class ReactTooltip extends Component {
/**
* Add scroll eventlistener when tooltip show
- * or tooltip will always existed
+ * automatically hide the tooltip when scrolling
*/
- addScrollListener () {
- window.addEventListener('scroll', this.hideTooltip)
+ addScrollListener (e) {
+ const isCaptureMode = this.isCapture(e.currentTarget)
+ window.addEventListener('scroll', ::this.hideTooltip, isCaptureMode)
}
removeScrollListener () {
window.removeEventListener('scroll', this.hideTooltip)
}
- /**
- * Get tooltip poisition by current target
- */
- getPosition (currentTarget) {
- const {place} = this.state
- const node = ReactDOM.findDOMNode(this)
- const boundingClientRect = currentTarget.getBoundingClientRect()
- const targetTop = boundingClientRect.top
- const targetLeft = boundingClientRect.left
- const tipWidth = node.clientWidth
- const tipHeight = node.clientHeight
- const targetWidth = currentTarget.clientWidth
- const targetHeight = currentTarget.clientHeight
- const windoWidth = window.innerWidth
- const windowHeight = window.innerHeight
- let x
- let y
- const defaultTopY = targetTop - tipHeight - 8
- const defaultBottomY = targetTop + targetHeight + 8
- const defaultLeftX = targetLeft - tipWidth - 6
- const defaultRightX = targetLeft + targetWidth + 6
-
- let parentTop = 0
- let parentLeft = 0
- let currentParent = currentTarget.parentElement
-
- while (currentParent) {
- if (currentParent.style.transform.length > 0) {
- break
- }
- currentParent = currentParent.parentElement
- }
-
- if (currentParent) {
- parentTop = currentParent.getBoundingClientRect().top
- parentLeft = currentParent.getBoundingClientRect().left
- }
-
- const outsideTop = () => {
- return defaultTopY - 10 < 0
- }
-
- const outsideBottom = () => {
- return targetTop + targetHeight + tipHeight + 25 > windowHeight
- }
-
- const outsideLeft = () => {
- return defaultLeftX - 10 < 0
- }
-
- const outsideRight = () => {
- return targetLeft + targetWidth + tipWidth + 25 > windoWidth
- }
-
- const getTopPositionY = () => {
- if (outsideTop(defaultTopY) && !outsideBottom()) {
- this.setState({
- place: 'bottom'
- })
- return defaultBottomY
- }
-
- return defaultTopY
- }
-
- const getBottomPositionY = () => {
- if (outsideBottom() && !outsideTop()) {
- this.setState({
- place: 'top'
- })
- return defaultTopY
- }
-
- return defaultBottomY
- }
-
- const getLeftPositionX = () => {
- if (outsideLeft() && !outsideRight()) {
- this.setState({
- place: 'right'
- })
- return defaultRightX
- }
-
- return defaultLeftX
- }
-
- const getRightPositionX = () => {
- if (outsideRight() && !outsideLeft()) {
- this.setState({
- place: 'left'
- })
- return defaultLeftX
- }
-
- return defaultRightX
- }
-
- if (place === 'top') {
- x = targetLeft - (tipWidth / 2) + (targetWidth / 2) - parentLeft
- y = getTopPositionY() - parentTop
- } else if (place === 'bottom') {
- x = targetLeft - (tipWidth / 2) + (targetWidth / 2) - parentLeft
- y = getBottomPositionY() - parentTop
- } else if (place === 'left') {
- x = getLeftPositionX() - parentLeft
- y = targetTop + (targetHeight / 2) - (tipHeight / 2) - parentTop
- } else if (place === 'right') {
- x = getRightPositionX() - parentLeft
- y = targetTop + (targetHeight / 2) - (tipHeight / 2) - parentTop
- }
-
- return { x, y }
- }
-
- /**
- * Execute in componentDidUpdate, can't put this into render() to support server rendering
- */
+ // Calculation the position
updatePosition () {
- let node = ReactDOM.findDOMNode(this)
-
- let tipWidth = node.clientWidth
- let tipHeight = node.clientHeight
- let { effect, place, offset } = this.state
- let offsetFromEffect = {}
-
- /**
- * List all situations for different placement,
- * then tooltip can judge switch to which side if window space is not enough
- * @note only support for float at the moment
- */
- const placements = ['top', 'bottom', 'left', 'right']
- placements.forEach(key => {
- offsetFromEffect[key] = {x: 0, y: 0}
- })
-
- if (effect === 'float') {
- offsetFromEffect.top = {
- x: -(tipWidth / 2),
- y: -tipHeight
- }
- offsetFromEffect.bottom = {
- x: -(tipWidth / 2),
- y: 15
- }
- offsetFromEffect.left = {
- x: -(tipWidth + 15),
- y: -(tipHeight / 2)
- }
- offsetFromEffect.right = {
- x: 10,
- y: -(tipHeight / 2)
- }
- }
-
- let xPosition = 0
- let yPosition = 0
-
- /* If user set offset attribute, we have to consider it into out position calculating */
- if (Object.prototype.toString.apply(offset) === '[object String]') {
- offset = JSON.parse(offset.toString().replace(/\'/g, '\"'))
- }
- for (let key in offset) {
- if (key === 'top') {
- yPosition -= parseInt(offset[key], 10)
- } else if (key === 'bottom') {
- yPosition += parseInt(offset[key], 10)
- } else if (key === 'left') {
- xPosition -= parseInt(offset[key], 10)
- } else if (key === 'right') {
- xPosition += parseInt(offset[key], 10)
- }
- }
-
- /* If our tooltip goes outside the window we want to try and change its place to be inside the window */
- let x = this.state.x
- let y = this.state.y
- const windoWidth = window.innerWidth
- const windowHeight = window.innerHeight
-
- const getStyleLeft = (place) => {
- const offsetEffectX = effect === 'solid' ? 0 : place ? offsetFromEffect[place].x : 0
- return x + offsetEffectX + xPosition
- }
- const getStyleTop = (place) => {
- const offsetEffectY = effect === 'solid' ? 0 : place ? offsetFromEffect[place].y : 0
- return y + offsetEffectY + yPosition
- }
-
- const outsideLeft = (place) => {
- const styleLeft = getStyleLeft(place)
- return styleLeft < 0 && x + offsetFromEffect['right'].x + xPosition <= windoWidth
- }
- const outsideRight = (place) => {
- const styleLeft = getStyleLeft(place)
- return styleLeft + tipWidth > windoWidth && x + offsetFromEffect['left'].x + xPosition >= 0
- }
- const outsideTop = (place) => {
- const styleTop = getStyleTop(place)
- return styleTop < 0 && y + offsetFromEffect['bottom'].y + yPosition + tipHeight < windowHeight
- }
- const outsideBottom = (place) => {
- var styleTop = getStyleTop(place)
- return styleTop + tipHeight >= windowHeight && y + offsetFromEffect['top'].y + yPosition >= 0
- }
-
- /* We want to make sure the place we switch to will not go outside either */
- const outside = (place) => {
- return outsideTop(place) || outsideRight(place) || outsideBottom(place) || outsideLeft(place)
- }
+ const {currentEvent, currentTarget, place, effect, offset} = this.state
+ const node = ReactDOM.findDOMNode(this)
- /* We check each side and switch if the new place will be in bounds */
- if (outsideLeft(place)) {
- if (!outside('right')) {
- this.setState({
- place: 'right'
- })
- return
- }
- } else if (outsideRight(place)) {
- if (!outside('left')) {
- this.setState({
- place: 'left'
- })
- return
- }
- } else if (outsideTop(place)) {
- if (!outside('bottom')) {
- this.setState({
- place: 'bottom'
- })
- return
- }
- } else if (outsideBottom(place)) {
- if (!outside('top')) {
- this.setState({
- place: 'top'
- })
- return
- }
+ const result = getPosition(currentEvent, currentTarget, node, place, effect, offset)
+ if (result.isNewState) {
+ // Switch to reverse placement
+ return this.setState(result.newState, () => {
+ this.updatePosition()
+ })
}
-
- node.style.left = getStyleLeft(place) + 'px'
- node.style.top = getStyleTop(place) + 'px'
+ // Set tooltip position
+ node.style.left = result.position.left + 'px'
+ node.style.top = result.position.top + 'px'
}
/**
* Set style tag in header
- * Insert style by this way
+ * in this way we can insert default css
*/
setStyleHeader () {
if (!document.getElementsByTagName('head')[0].querySelector('style[id="react-tooltip"]')) {
@@ -623,59 +292,17 @@ class ReactTooltip extends Component {
if (html) {
return (
-
+
)
} else {
- const content = this.props.children ? this.props.children : placeholder
return (
- {content}
+ {placeholder}
)
}
}
-
- trim (string) {
- if (Object.prototype.toString.call(string) !== '[object String]') {
- return string
- }
- let newString = string.split('')
- let firstCount = 0
- let lastCount = 0
- for (let i = 0; i < string.length; i++) {
- if (string[i] !== ' ') {
- break
- }
- firstCount++
- }
- for (let i = string.length - 1; i >= 0; i--) {
- if (string[i] !== ' ') {
- break
- }
- lastCount++
- }
- newString.splice(0, firstCount)
- newString.splice(-lastCount, lastCount)
- return newString.join('')
- }
-
-}
-
-ReactTooltip.propTypes = {
- children: PropTypes.any,
- place: PropTypes.string,
- type: PropTypes.string,
- effect: PropTypes.string,
- offset: PropTypes.object,
- multiline: PropTypes.bool,
- border: PropTypes.bool,
- class: PropTypes.string,
- id: PropTypes.string,
- html: PropTypes.bool,
- delayHide: PropTypes.number,
- delayShow: PropTypes.number,
- event: PropTypes.any,
- eventOff: PropTypes.any,
- watchWindow: PropTypes.bool,
- isCapture: PropTypes.bool
}
/* export default not fit for standalone, it will exports {default:...} */
diff --git a/src/index.scss b/src/index.scss
index 53b2af939..fd0c2b7a8 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -2,22 +2,30 @@
background-color: $background-color;
&.place-top {
&:after {
- border-top: 6px solid $background-color;
+ border-top-color: $background-color;
+ border-top-style: solid;
+ border-top-width: 6px;
}
}
&.place-bottom {
&:after {
- border-bottom: 6px solid $background-color;
+ border-bottom-color: $background-color;
+ border-bottom-style: solid;
+ border-bottom-width: 6px;
}
}
&.place-left {
&:after {
- border-left: 6px solid $background-color;
+ border-left-color: $background-color;
+ border-left-style: solid;
+ border-left-width: 6px;
}
}
&.place-right {
&:after {
- border-right: 6px solid $background-color;
+ border-right-color: $background-color;
+ border-right-style: solid;
+ border-right-width: 6px;
}
}
}
@@ -188,4 +196,4 @@
padding: 2px 0px;
text-align: center;
}
-}
+}
diff --git a/src/style.js b/src/style.js
index b9f64aaf2..cd4bb4da3 100644
--- a/src/style.js
+++ b/src/style.js
@@ -1 +1 @@
-export default '.__react_component_tooltip{border-radius:3px;display:inline-block;font-size:13px;left:-999em;opacity:0;padding:8px 21px;position:fixed;pointer-events:none;transition:opacity 0.3s ease-out , margin-top 0.3s ease-out, margin-left 0.3s ease-out;top:-999em;visibility:hidden;z-index:999}.__react_component_tooltip:before,.__react_component_tooltip:after{content:"";width:0;height:0;position:absolute}.__react_component_tooltip.show{opacity:0.9;margin-top:0px;margin-left:0px;visibility:visible}.__react_component_tooltip.type-dark{color:#fff;background-color:#222}.__react_component_tooltip.type-dark.place-top:after{border-top:6px solid #222}.__react_component_tooltip.type-dark.place-bottom:after{border-bottom:6px solid #222}.__react_component_tooltip.type-dark.place-left:after{border-left:6px solid #222}.__react_component_tooltip.type-dark.place-right:after{border-right:6px solid #222}.__react_component_tooltip.type-dark.border{border:1px solid #fff}.__react_component_tooltip.type-dark.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-dark.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-dark.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-dark.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-success{color:#fff;background-color:#8DC572}.__react_component_tooltip.type-success.place-top:after{border-top:6px solid #8DC572}.__react_component_tooltip.type-success.place-bottom:after{border-bottom:6px solid #8DC572}.__react_component_tooltip.type-success.place-left:after{border-left:6px solid #8DC572}.__react_component_tooltip.type-success.place-right:after{border-right:6px solid #8DC572}.__react_component_tooltip.type-success.border{border:1px solid #fff}.__react_component_tooltip.type-success.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-success.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-success.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-success.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-warning{color:#fff;background-color:#F0AD4E}.__react_component_tooltip.type-warning.place-top:after{border-top:6px solid #F0AD4E}.__react_component_tooltip.type-warning.place-bottom:after{border-bottom:6px solid #F0AD4E}.__react_component_tooltip.type-warning.place-left:after{border-left:6px solid #F0AD4E}.__react_component_tooltip.type-warning.place-right:after{border-right:6px solid #F0AD4E}.__react_component_tooltip.type-warning.border{border:1px solid #fff}.__react_component_tooltip.type-warning.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-warning.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-warning.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-warning.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-error{color:#fff;background-color:#BE6464}.__react_component_tooltip.type-error.place-top:after{border-top:6px solid #BE6464}.__react_component_tooltip.type-error.place-bottom:after{border-bottom:6px solid #BE6464}.__react_component_tooltip.type-error.place-left:after{border-left:6px solid #BE6464}.__react_component_tooltip.type-error.place-right:after{border-right:6px solid #BE6464}.__react_component_tooltip.type-error.border{border:1px solid #fff}.__react_component_tooltip.type-error.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-error.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-error.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-error.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-info{color:#fff;background-color:#337AB7}.__react_component_tooltip.type-info.place-top:after{border-top:6px solid #337AB7}.__react_component_tooltip.type-info.place-bottom:after{border-bottom:6px solid #337AB7}.__react_component_tooltip.type-info.place-left:after{border-left:6px solid #337AB7}.__react_component_tooltip.type-info.place-right:after{border-right:6px solid #337AB7}.__react_component_tooltip.type-info.border{border:1px solid #fff}.__react_component_tooltip.type-info.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-info.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-info.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-info.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-light{color:#222;background-color:#fff}.__react_component_tooltip.type-light.place-top:after{border-top:6px solid #fff}.__react_component_tooltip.type-light.place-bottom:after{border-bottom:6px solid #fff}.__react_component_tooltip.type-light.place-left:after{border-left:6px solid #fff}.__react_component_tooltip.type-light.place-right:after{border-right:6px solid #fff}.__react_component_tooltip.type-light.border{border:1px solid #222}.__react_component_tooltip.type-light.border.place-top:before{border-top:8px solid #222}.__react_component_tooltip.type-light.border.place-bottom:before{border-bottom:8px solid #222}.__react_component_tooltip.type-light.border.place-left:before{border-left:8px solid #222}.__react_component_tooltip.type-light.border.place-right:before{border-right:8px solid #222}.__react_component_tooltip.place-top{margin-top:-10px}.__react_component_tooltip.place-top:before{border-left:10px solid transparent;border-right:10px solid transparent;bottom:-8px;left:50%;margin-left:-10px}.__react_component_tooltip.place-top:after{border-left:8px solid transparent;border-right:8px solid transparent;bottom:-6px;left:50%;margin-left:-8px}.__react_component_tooltip.place-bottom{margin-top:10px}.__react_component_tooltip.place-bottom:before{border-left:10px solid transparent;border-right:10px solid transparent;top:-8px;left:50%;margin-left:-10px}.__react_component_tooltip.place-bottom:after{border-left:8px solid transparent;border-right:8px solid transparent;top:-6px;left:50%;margin-left:-8px}.__react_component_tooltip.place-left{margin-left:-10px}.__react_component_tooltip.place-left:before{border-top:6px solid transparent;border-bottom:6px solid transparent;right:-8px;top:50%;margin-top:-5px}.__react_component_tooltip.place-left:after{border-top:5px solid transparent;border-bottom:5px solid transparent;right:-6px;top:50%;margin-top:-4px}.__react_component_tooltip.place-right{margin-left:10px}.__react_component_tooltip.place-right:before{border-top:6px solid transparent;border-bottom:6px solid transparent;left:-8px;top:50%;margin-top:-5px}.__react_component_tooltip.place-right:after{border-top:5px solid transparent;border-bottom:5px solid transparent;left:-6px;top:50%;margin-top:-4px}.__react_component_tooltip .multi-line{display:block;padding:2px 0px;text-align:center}'
\ No newline at end of file
+export default '.__react_component_tooltip{border-radius:3px;display:inline-block;font-size:13px;left:-999em;opacity:0;padding:8px 21px;position:fixed;pointer-events:none;transition:opacity 0.3s ease-out , margin-top 0.3s ease-out, margin-left 0.3s ease-out;top:-999em;visibility:hidden;z-index:999}.__react_component_tooltip:before,.__react_component_tooltip:after{content:"";width:0;height:0;position:absolute}.__react_component_tooltip.show{opacity:0.9;margin-top:0px;margin-left:0px;visibility:visible}.__react_component_tooltip.type-dark{color:#fff;background-color:#222}.__react_component_tooltip.type-dark.place-top:after{border-top-color:#222;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-dark.place-bottom:after{border-bottom-color:#222;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-dark.place-left:after{border-left-color:#222;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-dark.place-right:after{border-right-color:#222;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-dark.border{border:1px solid #fff}.__react_component_tooltip.type-dark.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-dark.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-dark.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-dark.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-success{color:#fff;background-color:#8DC572}.__react_component_tooltip.type-success.place-top:after{border-top-color:#8DC572;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-success.place-bottom:after{border-bottom-color:#8DC572;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-success.place-left:after{border-left-color:#8DC572;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-success.place-right:after{border-right-color:#8DC572;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-success.border{border:1px solid #fff}.__react_component_tooltip.type-success.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-success.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-success.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-success.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-warning{color:#fff;background-color:#F0AD4E}.__react_component_tooltip.type-warning.place-top:after{border-top-color:#F0AD4E;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-warning.place-bottom:after{border-bottom-color:#F0AD4E;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-warning.place-left:after{border-left-color:#F0AD4E;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-warning.place-right:after{border-right-color:#F0AD4E;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-warning.border{border:1px solid #fff}.__react_component_tooltip.type-warning.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-warning.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-warning.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-warning.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-error{color:#fff;background-color:#BE6464}.__react_component_tooltip.type-error.place-top:after{border-top-color:#BE6464;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-error.place-bottom:after{border-bottom-color:#BE6464;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-error.place-left:after{border-left-color:#BE6464;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-error.place-right:after{border-right-color:#BE6464;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-error.border{border:1px solid #fff}.__react_component_tooltip.type-error.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-error.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-error.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-error.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-info{color:#fff;background-color:#337AB7}.__react_component_tooltip.type-info.place-top:after{border-top-color:#337AB7;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-info.place-bottom:after{border-bottom-color:#337AB7;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-info.place-left:after{border-left-color:#337AB7;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-info.place-right:after{border-right-color:#337AB7;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-info.border{border:1px solid #fff}.__react_component_tooltip.type-info.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-info.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-info.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-info.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-light{color:#222;background-color:#fff}.__react_component_tooltip.type-light.place-top:after{border-top-color:#fff;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-light.place-bottom:after{border-bottom-color:#fff;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-light.place-left:after{border-left-color:#fff;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-light.place-right:after{border-right-color:#fff;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-light.border{border:1px solid #222}.__react_component_tooltip.type-light.border.place-top:before{border-top:8px solid #222}.__react_component_tooltip.type-light.border.place-bottom:before{border-bottom:8px solid #222}.__react_component_tooltip.type-light.border.place-left:before{border-left:8px solid #222}.__react_component_tooltip.type-light.border.place-right:before{border-right:8px solid #222}.__react_component_tooltip.place-top{margin-top:-10px}.__react_component_tooltip.place-top:before{border-left:10px solid transparent;border-right:10px solid transparent;bottom:-8px;left:50%;margin-left:-10px}.__react_component_tooltip.place-top:after{border-left:8px solid transparent;border-right:8px solid transparent;bottom:-6px;left:50%;margin-left:-8px}.__react_component_tooltip.place-bottom{margin-top:10px}.__react_component_tooltip.place-bottom:before{border-left:10px solid transparent;border-right:10px solid transparent;top:-8px;left:50%;margin-left:-10px}.__react_component_tooltip.place-bottom:after{border-left:8px solid transparent;border-right:8px solid transparent;top:-6px;left:50%;margin-left:-8px}.__react_component_tooltip.place-left{margin-left:-10px}.__react_component_tooltip.place-left:before{border-top:6px solid transparent;border-bottom:6px solid transparent;right:-8px;top:50%;margin-top:-5px}.__react_component_tooltip.place-left:after{border-top:5px solid transparent;border-bottom:5px solid transparent;right:-6px;top:50%;margin-top:-4px}.__react_component_tooltip.place-right{margin-left:10px}.__react_component_tooltip.place-right:before{border-top:6px solid transparent;border-bottom:6px solid transparent;left:-8px;top:50%;margin-top:-5px}.__react_component_tooltip.place-right:after{border-top:5px solid transparent;border-bottom:5px solid transparent;left:-6px;top:50%;margin-top:-4px}.__react_component_tooltip .multi-line{display:block;padding:2px 0px;text-align:center}'
\ No newline at end of file
diff --git a/src/utils/getPosition.js b/src/utils/getPosition.js
new file mode 100644
index 000000000..109eb7fc9
--- /dev/null
+++ b/src/utils/getPosition.js
@@ -0,0 +1,151 @@
+/**
+ * Calculate the position of tooltip
+ *
+ * @params
+ * - `e` {Event} the event of current mouse
+ * - `target` {Element} the currentTarget of the event
+ * - `node` {DOM} the react-tooltip object
+ * - `place` {String} top / right / bottom / left
+ * - `effect` {String} float / solid
+ * - `offset` {Object} the offset to default position
+ *
+ * @return {Object
+ * - `isNewState` {Bool} required
+ * - `newState` {Object}
+ * - `position` {OBject} {left: {Number}, top: {Number}}
+ */
+export default function (e, target, node, place, effect, offset) {
+ const tipWidth = node.clientWidth
+ const tipHeight = node.clientHeight
+ const {mouseX, mouseY} = getCurrentOffset(e, target, effect)
+ const defaultOffset = getDefaultPosition(effect, target.clientWidth, target.clientHeight, tipWidth, tipHeight)
+ const {extraOffset_X, extraOffset_Y} = calculateOffset(offset)
+
+ const widnowWidth = window.innerWidth
+ const windowHeight = window.innerHeight
+
+ // Get the edge offset of the tooltip
+ const getTipOffsetLeft = (place) => {
+ const offset_X = defaultOffset[place].x
+ return mouseX + offset_X + extraOffset_X
+ }
+ const getTipOffsetTop = (place) => {
+ const offset_Y = defaultOffset[place].y
+ return mouseY + offset_Y + extraOffset_Y
+ }
+
+ // Judge if the tooltip has over the window(screen)
+ const outsideLeft = () => {
+ // if switch to right will out of screen, return false because switch placement doesn't make sense
+ return getTipOffsetLeft('left') < 0 && getTipOffsetLeft('right') <= widnowWidth
+ }
+ const outsideRight = () => {
+ return getTipOffsetLeft('right') > widnowWidth && getTipOffsetLeft('left') >= 0
+ }
+ const outsideTop = () => {
+ return getTipOffsetTop('top') < 0 && getTipOffsetTop('bottom') + tipHeight <= windowHeight
+ }
+ const outsideBottom = () => {
+ return getTipOffsetTop('bottom') + tipHeight > windowHeight && getTipOffsetTop('top') >= 0
+ }
+
+ // Return new state to change the placement to the reverse if possible
+ if (place === 'left' && outsideLeft()) {
+ return {
+ isNewState: true,
+ newState: {place: 'right'}
+ }
+ } else if (place === 'right' && outsideRight()) {
+ return {
+ isNewState: true,
+ newState: {place: 'left'}
+ }
+ } else if (place === 'top' && outsideTop()) {
+ return {
+ isNewState: true,
+ newState: {place: 'bottom'}
+ }
+ } else if (place === 'bottom' && outsideBottom()) {
+ return {
+ isNewState: true,
+ newState: {place: 'top'}
+ }
+ }
+
+ // Return tooltip offset position
+ return {
+ isNewState: false,
+ position: {
+ left: getTipOffsetLeft(place),
+ top: getTipOffsetTop(place)
+ }
+ }
+}
+
+// Get current mouse offset
+const getCurrentOffset = (e, currentTarget, effect) => {
+ const boundingClientRect = currentTarget.getBoundingClientRect()
+ const targetTop = boundingClientRect.top
+ const targetLeft = boundingClientRect.left
+ const targetWidth = currentTarget.clientWidth
+ const targetHeight = currentTarget.clientHeight
+
+ if (effect === 'float') {
+ return {
+ mouseX: e.clientX,
+ mouseY: e.clientY
+ }
+ }
+ return {
+ mouseX: targetLeft + (targetWidth / 2),
+ mouseY: targetTop + (targetHeight / 2)
+ }
+}
+
+// List all possibility of tooltip final offset
+// This is useful in judging if it is necessary for tooltip to switch position when out of window
+const getDefaultPosition = (effect, targetWidth, targetHeight, tipWidth, tipHeight) => {
+ let top
+ let right
+ let bottom
+ let left
+ const disToMouse = 15
+ const triangleHeight = 5
+
+ if (effect === 'float') {
+ top = {x: -(tipWidth / 2), y: -(tipHeight + disToMouse - triangleHeight)}
+ bottom = {x: -(tipWidth / 2), y: disToMouse}
+ left = {x: -(tipWidth + disToMouse - triangleHeight), y: -(tipHeight / 2)}
+ right = {x: disToMouse, y: -(tipHeight / 2)}
+ } else if (effect === 'solid') {
+ top = {x: -(tipWidth / 2), y: -(targetHeight / 2 + tipHeight)}
+ bottom = {x: -(tipWidth / 2), y: targetHeight / 2}
+ left = {x: -(tipWidth + targetWidth / 2), y: -(tipHeight / 2)}
+ right = {x: targetWidth / 2, y: -(tipHeight / 2)}
+ }
+
+ return {top, bottom, left, right}
+}
+
+// Consider additional offset into position calculation
+const calculateOffset = (offset) => {
+ let extraOffset_X = 0
+ let extraOffset_Y = 0
+
+ if (Object.prototype.toString.apply(offset) === '[object String]') {
+ offset = JSON.parse(offset.toString().replace(/\'/g, '\"'))
+ }
+ for (let key in offset) {
+ if (key === 'top') {
+ extraOffset_Y -= parseInt(offset[key], 10)
+ } else if (key === 'bottom') {
+ extraOffset_Y += parseInt(offset[key], 10)
+ } else if (key === 'left') {
+ extraOffset_X -= parseInt(offset[key], 10)
+ } else if (key === 'right') {
+ extraOffset_X += parseInt(offset[key], 10)
+ }
+ }
+
+ return {extraOffset_X, extraOffset_Y}
+}
diff --git a/src/utils/getTipContent.js b/src/utils/getTipContent.js
new file mode 100644
index 000000000..774d77eb8
--- /dev/null
+++ b/src/utils/getTipContent.js
@@ -0,0 +1,30 @@
+/**
+ * To get the tooltip content
+ * it may comes from data-tip or this.props.children
+ * it should support multiline
+ *
+ * @params
+ * - `tip` {String} value of data-tip
+ * - `children` {ReactElement} this.props.children
+ * - `multiline` {Any} could be Bool(true/false) or String('true'/'false')
+ *
+ * @return
+ * - String or react component
+ */
+import React from 'react'
+
+export default function (tip, children, multiline) {
+ if (children) return children
+
+ const regexp = /
/
+ if (!multiline || multiline === 'false' || !regexp.test(tip)) {
+ return tip
+ }
+
+ // Multiline tooltip content
+ return tip.split(regexp).map((d, i) => {
+ return (
+ {d}
+ )
+ })
+}
diff --git a/standalone/style.js b/standalone/style.js
new file mode 100644
index 000000000..62c4793b3
--- /dev/null
+++ b/standalone/style.js
@@ -0,0 +1,6 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = '.__react_component_tooltip{border-radius:3px;display:inline-block;font-size:13px;left:-999em;opacity:0;padding:8px 21px;position:fixed;pointer-events:none;transition:opacity 0.3s ease-out , margin-top 0.3s ease-out, margin-left 0.3s ease-out;top:-999em;visibility:hidden;z-index:999}.__react_component_tooltip:before,.__react_component_tooltip:after{content:"";width:0;height:0;position:absolute}.__react_component_tooltip.show{opacity:0.9;margin-top:0px;margin-left:0px;visibility:visible}.__react_component_tooltip.type-dark{color:#fff;background-color:#222}.__react_component_tooltip.type-dark.place-top:after{border-top:6px solid #222}.__react_component_tooltip.type-dark.place-bottom:after{border-bottom:6px solid #222}.__react_component_tooltip.type-dark.place-left:after{border-left:6px solid #222}.__react_component_tooltip.type-dark.place-right:after{border-right:6px solid #222}.__react_component_tooltip.type-dark.border{border:1px solid #fff}.__react_component_tooltip.type-dark.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-dark.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-dark.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-dark.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-success{color:#fff;background-color:#8DC572}.__react_component_tooltip.type-success.place-top:after{border-top:6px solid #8DC572}.__react_component_tooltip.type-success.place-bottom:after{border-bottom:6px solid #8DC572}.__react_component_tooltip.type-success.place-left:after{border-left:6px solid #8DC572}.__react_component_tooltip.type-success.place-right:after{border-right:6px solid #8DC572}.__react_component_tooltip.type-success.border{border:1px solid #fff}.__react_component_tooltip.type-success.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-success.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-success.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-success.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-warning{color:#fff;background-color:#F0AD4E}.__react_component_tooltip.type-warning.place-top:after{border-top:6px solid #F0AD4E}.__react_component_tooltip.type-warning.place-bottom:after{border-bottom:6px solid #F0AD4E}.__react_component_tooltip.type-warning.place-left:after{border-left:6px solid #F0AD4E}.__react_component_tooltip.type-warning.place-right:after{border-right:6px solid #F0AD4E}.__react_component_tooltip.type-warning.border{border:1px solid #fff}.__react_component_tooltip.type-warning.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-warning.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-warning.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-warning.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-error{color:#fff;background-color:#BE6464}.__react_component_tooltip.type-error.place-top:after{border-top:6px solid #BE6464}.__react_component_tooltip.type-error.place-bottom:after{border-bottom:6px solid #BE6464}.__react_component_tooltip.type-error.place-left:after{border-left:6px solid #BE6464}.__react_component_tooltip.type-error.place-right:after{border-right:6px solid #BE6464}.__react_component_tooltip.type-error.border{border:1px solid #fff}.__react_component_tooltip.type-error.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-error.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-error.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-error.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-info{color:#fff;background-color:#337AB7}.__react_component_tooltip.type-info.place-top:after{border-top:6px solid #337AB7}.__react_component_tooltip.type-info.place-bottom:after{border-bottom:6px solid #337AB7}.__react_component_tooltip.type-info.place-left:after{border-left:6px solid #337AB7}.__react_component_tooltip.type-info.place-right:after{border-right:6px solid #337AB7}.__react_component_tooltip.type-info.border{border:1px solid #fff}.__react_component_tooltip.type-info.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-info.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-info.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-info.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-light{color:#222;background-color:#fff}.__react_component_tooltip.type-light.place-top:after{border-top:6px solid #fff}.__react_component_tooltip.type-light.place-bottom:after{border-bottom:6px solid #fff}.__react_component_tooltip.type-light.place-left:after{border-left:6px solid #fff}.__react_component_tooltip.type-light.place-right:after{border-right:6px solid #fff}.__react_component_tooltip.type-light.border{border:1px solid #222}.__react_component_tooltip.type-light.border.place-top:before{border-top:8px solid #222}.__react_component_tooltip.type-light.border.place-bottom:before{border-bottom:8px solid #222}.__react_component_tooltip.type-light.border.place-left:before{border-left:8px solid #222}.__react_component_tooltip.type-light.border.place-right:before{border-right:8px solid #222}.__react_component_tooltip.place-top{margin-top:-10px}.__react_component_tooltip.place-top:before{border-left:10px solid transparent;border-right:10px solid transparent;bottom:-8px;left:50%;margin-left:-10px}.__react_component_tooltip.place-top:after{border-left:8px solid transparent;border-right:8px solid transparent;bottom:-6px;left:50%;margin-left:-8px}.__react_component_tooltip.place-bottom{margin-top:10px}.__react_component_tooltip.place-bottom:before{border-left:10px solid transparent;border-right:10px solid transparent;top:-8px;left:50%;margin-left:-10px}.__react_component_tooltip.place-bottom:after{border-left:8px solid transparent;border-right:8px solid transparent;top:-6px;left:50%;margin-left:-8px}.__react_component_tooltip.place-left{margin-left:-10px}.__react_component_tooltip.place-left:before{border-top:6px solid transparent;border-bottom:6px solid transparent;right:-8px;top:50%;margin-top:-5px}.__react_component_tooltip.place-left:after{border-top:5px solid transparent;border-bottom:5px solid transparent;right:-6px;top:50%;margin-top:-4px}.__react_component_tooltip.place-right{margin-left:10px}.__react_component_tooltip.place-right:before{border-top:6px solid transparent;border-bottom:6px solid transparent;left:-8px;top:50%;margin-top:-5px}.__react_component_tooltip.place-right:after{border-top:5px solid transparent;border-bottom:5px solid transparent;left:-6px;top:50%;margin-top:-4px}.__react_component_tooltip .multi-line{display:block;padding:2px 0px;text-align:center}';
diff --git a/test/globalMethods.test.js b/test/globalMethods.test.js
new file mode 100644
index 000000000..fdcdb3f34
--- /dev/null
+++ b/test/globalMethods.test.js
@@ -0,0 +1,37 @@
+/* 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', () => {
+ before(() => {
+ sinon.spy(ReactTooltip.prototype, 'hideTooltip')
+ sinon.spy(ReactTooltip.prototype, 'globalRebuild')
+ })
+
+ it('should be hided by invoking ReactTooltip.hide', done => {
+ const wrapper = mount()
+ wrapper.setState({ show: true })
+ ReactTooltip.hide()
+ setImmediate(() => {
+ expect(ReactTooltip.prototype.hideTooltip.calledOnce).to.equal(true)
+ expect(wrapper).to.have.state('show', false)
+ done()
+ })
+ })
+
+ it('should invoke globalRebuild when using ReactTooltip.rebuild', done => {
+ ReactTooltip.rebuild()
+ setImmediate(() => {
+ expect(ReactTooltip.prototype.globalRebuild.calledOnce).to.equal(true)
+ done()
+ })
+ })
+})
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'
+}