diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..4687bc4 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "react", "stage-0"] +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..8d87b1d --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +node_modules/* diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..cd99e01 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,153 @@ +{ + "env": { + "browser": true, + "node": true + }, + + "parser": "babel-eslint", + + "plugins": [ + "react" + ], + + "ecmaFeatures": { + "jsx": true + }, + + "rules": { + // For a complete reference, check out http://eslint.org/docs/rules/ + + //------------------------------------------------------------------------- + // Best practices + //------------------------------------------------------------------------- + + // Prevent abbreviated blocks for clarity + "curly": 2, + + // Enforce a reasonable cap on functions spiralling out of control + // with many branches. + "complexity": [2, 10], + + // Prefer foo.x over foo['x']; static properties are always preferable + // to dynamic strings. + "dot-notation": 2, + + "dot-location": [2, "property"], + + // enforce === and !== for comparisons + "eqeqeq": [2, "smart"], + + "guard-for-in": 2, + + "no-floating-decimal": 2, + + // Avoid funny things with parseInt. + // See: http://stackoverflow.com/questions/850341 + "radix": 2, + + // Avoid pitfalls when trying to call a just-declared function. + "wrap-iife": 2, + + // May the force be with you. + "yoda": [0, "always"], + + //------------------------------------------------------------------------- + // Strict Mode + //------------------------------------------------------------------------- + + // Transpilers deal with the effects of strict, so ignore it. + "strict": [2, "never"], + + //------------------------------------------------------------------------- + // Variable declaration + //------------------------------------------------------------------------- + "no-use-before-define": 2, + + "no-undefined": 2, + + "no-unused-vars": 2, + + //------------------------------------------------------------------------- + // Code style + //------------------------------------------------------------------------- + + // The one true brace style. + "brace-style": [2, "1tbs"], + + "camelcase": 0, + + "comma-spacing": [2, { "before": false, "after": true }], + + "consistent-this": [2, "_this"], + + "eol-last": 2, + + "indent": [2, 2], + + "key-spacing": [2, { "beforeColon": false, "afterColon": true }], + + "new-cap": 2, + + "no-lonely-if": 2, + + "no-mixed-spaces-and-tabs": [2, true], + + "no-multiple-empty-lines": 2, + + // Nested ternaries are just plain confusing. Avoiding them keeps the + // code readable. + "no-nested-ternary": 2, + + // There are no such thing as "private" properties. Use closure + // variables if you really need isolation. + "no-underscore-dangle": 0, + + "no-spaced-func": 2, + + // use one variable declaration for each variable you want to define + "one-var": [2, "never"], + + // enforce double quotes + "quotes": [2, "double"], + + // Enforce whitespace for visual clarity. + "space-after-keywords": [2, "always"], + "spaced-comment": [2, "always", { "exceptions": ["-"] }], + + //------------------------------------------------------------------------- + // ECMAScript 6 + //------------------------------------------------------------------------- + + // Enforce `const` and `let` to describe what's going on. + "no-var": 2, + + //------------------------------------------------------------------------- + // React + //------------------------------------------------------------------------- + "jsx-quotes": [2, "prefer-double"], + "react/jsx-uses-vars": 2, + "react/jsx-uses-react": 2, + + // Display name is not needed when using ES6-style components + "react/display-name": 0, + + // Make things consistent and readable – prefer `x={true}` over `x` + "react/jsx-boolean-value": 2, + + // Keep the quote style consistent with Javascript land + "react/jsx-no-undef": 2, + "react/jsx-sort-props": 0, + "react/jsx-sort-prop-types": 0, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/no-multi-comp": 2, + "react/no-unknown-property": 2, + "react/react-in-jsx-scope": 1, + "react/self-closing-comp": 2, + "react/wrap-multilines": 2, + + // Potential issue in React ESLint package- receiving 'Can't add property + // react, object is not extensible' when used with eslint-loader. + "react/prop-types": 0 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..3f363a7 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +node_modules/ +build/* diff --git a/README.md b/README.md index 1f3f1ec..6874e41 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@ # FlowTip -*A flexible, adaptable and easy to use tooltip positioning library.* +*A flexible, adaptable and easy to use tooltip component for React.js* -* [Website](http://qiushihe.github.io/flowtip) -* [Interactive Demo](http://qiushihe.github.io/flowtip/demo.html) -* [Github Repo](https://github.com/qiushihe/flowtip) +Looking for the non-React.js version? It's in the [v1](v1) folder. ## Dependencies -* [jQuery](http://jquery.com) -* [Unerscore.js](http://underscorejs.org) -* [CoffeeScript](http://coffeescript.org) (development only) +* React +* ReactDOM ## Glossaries @@ -38,40 +35,35 @@ then in term aligned to the pivot. ## Basic Usage -Creates an instance of the **FlowTip** object: +To include an instance of FlowTip in your component: - var tooltip = new FlowTip(); +``` + + Holy Shit! +
+ FlowTip as React Component +
+``` -An instance of the **FlowTip** can be created with options (see **Attributes** section below): +The `target` and `parent` properties specifies the position and dimension of the tooltip's +target and parent, and for `parent` scroll positions should be included as well: - var myFlowTip = new FlowTip({ - region: "bottom" - className: "my-tip" - hasTail: false - }) +``` +targetProperties = { + top: ##, left: ##, width: ##, height: ## +}; -At this point, the tooltip is not yet "attached" to a target. The tooltip's target can be set by -calling `setTarget`: +parentProperties = { + top: ##, left: ##, width: ##, height: ##, + scrollTop: ##, scrollLeft: ## +}; +``` - tooltip.setTarget(tooltipTarget); +... where all `##` above should be integer values (i.e. not `##px`). -... where `tooltipTarget` may be a DOM object or a jQuery selection. +For possible values of `flowtipProperties` see [FlowTip Properties](#flowtip-properties) below. -To make the tooltip visible, call `show`: - - tooltip.show(); - -This will render the tooltip is it's not already rendered, and position the tooltip in the -appropriate region (see `region` in **Attributes** section below) with proper alignments -(see **Alignments** section below). - -It's important to note that instances of **FlowTip** does **not** automatically re-position -themselves when their targets move. The responsibility of detecting target movement lies outside -the scope of this library. To update the tooltip's position against its target: - - tooltip.reposition(); - -## Attributes +## FlowTip Properties ### `className`: **String** @@ -91,24 +83,6 @@ _Default value: ""_ Additional class name(s) for the tooltip's tail. -### `appendTo`: **Element** - -_Default value: null_ - -The element within which the tooltip will be inserted into. Can be set or updated by calling -`setAppendTo`. Default value is the document's `body` and the tooltip would thus be free to appear -and move anywhere on the page and edge detection will only be performed on the edge of the page. - -If `appendTo` is set to an element then edge detection will be performed on the edge of -the element instead. - -### `tooltipContent`: **String** or **Element** - -_Default value: null_ - -The content of the tooltip. May be specified as (HTML) string or DOM element. Can be set original -updated by calling `setTooltipContent`. - ### `region`: **String** _Default value: top_ @@ -273,56 +247,6 @@ pivot. When this value is positive, the clockwise edge will be used, and the cou edge will be used when this value is negative. The absolute value of this value controls the amount of shifting. -## Public Methods - -### `constructor` - -Accepts a hash of options. See **Attributes**. - -### `render` - -Render the tooltip's root (if not already rendered), content and insert the tooltip into the DOM. -If called repeatedly, only re-render the tooltip's content. - -### `setAppendTo` - -Set the tooltip's containing element. If the tooltip has already been rendered, the tooltip will -be moved/inserted into the new containing element. - -### `setTarget` - -Set the tooltip's target. The target is the element to which the tooltip will be pointing at. - -### `setClientRectTarget` - -Set the tooltip's target to be a ClientRect instead of a html element. The target is the -ClientRect to which the tooltip will be pointing at. - -### `setTooltipContent` - -Set the tooltip's content. If `render` is set to `true` for `options`, the `render()` method will -be called to re-render the content. - -### `reposition` - -Perform edge detection and position calculations to update the tooltip's position. - -### `show` - -Show the tooltip. Also update the tooltip's `visible` attribute. - -### `hide` - -Hide the tooltip. Also update the tooltip's `visible` attribute. - -### `trigger` - -Trigger an event from the root of the tooltip. - -### `destroy` - -Remove the tooltip's root from DOM. - ## Edge Detection There are three strategies for detection: **flip**, **rotate** and **squeeze**. @@ -362,8 +286,3 @@ Target alignment refers to the alignment of the pivot relative to the target of Root alignment refers to the alignment of the tooltip's root relative to the pivot. See `rootAlign` and `rootAlignOffset`. - -## Coordinator - -_TODO: Add documentation for `FlowTip.Coordinator`_ - diff --git a/demo/.DS_Store b/demo/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/demo/.DS_Store differ diff --git a/demo/index.css b/demo/index.css new file mode 100644 index 0000000..3c3b4b1 --- /dev/null +++ b/demo/index.css @@ -0,0 +1,21 @@ +body { + position: relative; + margin: 0px; + padding: 0px; +} + +.flowtipDemoArea { + background-color: gainsboro; +} + +.flowtipDemoTarget { + background-color: black; +} + +.flowtip-tail { + background-color: red; +} + +.flowtip-root { + background-color: gray; +} diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..16b60c2 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,13 @@ + + + + FlowTip.React Demo + + + +
+ + + + + diff --git a/lib/flowtip.js b/lib/flowtip.js new file mode 100644 index 0000000..c208bee --- /dev/null +++ b/lib/flowtip.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("react"),require("react-dom")):"function"==typeof define&&define.amd?define(["react","react-dom"],e):"object"==typeof exports?exports.flowtip=e(require("react"),require("react-dom")):t.flowtip=e(t.react,t["react-dom"])}(this,function(t,e){return function(t){function e(o){if(i[o])return i[o].exports;var r=i[o]={exports:{},id:o,loaded:!1};return t[o].call(r.exports,r,r.exports,e),r.loaded=!0,r.exports}var i={};return e.m=t,e.c=i,e.p="",e(0)}([function(t,e,i){"use strict";function o(t){return t&&t.__esModule?t:{"default":t}}function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function s(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a=Object.assign||function(t){for(var e=1;ef;f++)l[f]=arguments[f];return i=o=n(this,(t=Object.getPrototypeOf(e)).call.apply(t,[this].concat(l))),o.state={region:o.props.region,position:{tail:{}}},s=i,n(o,s)}return s(e,t),l(e,[{key:"rootAlign",value:function(t){return this.props[t+"RootAlign"]||this.props.rootAlign}},{key:"rootAlignOffset",value:function(t){return this.props[t+"RootAlignOffset"]||this.props.rootAlignOffset}},{key:"targetAlign",value:function(t){return this.props[t+"TargetAlign"]||this.props.targetAlign}},{key:"targetAlignOffset",value:function(t){return this.props[t+"TargetAlignOffset"]||this.props.targetAlignOffset}},{key:"availableRegion",value:function(t){return!this.props[t+"Disabled"]}},{key:"fitsInRegion",value:function(t,e,i){var o=this.calculatePosition(e,i),r=this.rootDimension();return"top"===t?o.top-this.props.edgeOffset>=0:"bottom"===t?o.top+r.height+this.props.edgeOffset<=e.height:"left"===t?o.left-this.props.edgeOffset>=0:"right"===t?o.left+r.width+this.props.edgeOffset<=e.width:void 0}},{key:"availableAndFitsIn",value:function(t,e,i){i||(i=t[0]);var o=t[0];if(!t||t.length<=0)return this.state.region;var r=e[o].availables,n=e[o].fits,s=this.props.hideInDisabledRegions;return r&&n||!r&&s?o:this.availableAndFitsIn(t.slice(1),e,i)}},{key:"rootDimension",value:function(){return this.refs.root.getDimension()}},{key:"tailDimension",value:function(t){this._tailOriginalDimension||(this._tailOriginalDimension=this.refs.root.getTailOriginalDimension());var e={width:this._tailOriginalDimension.width,height:this._tailOriginalDimension.height};return"left"===t||"right"===t?{width:e.height,height:e.width}:e}},{key:"tailType",value:function(t){return"top"===t?"bottom":"bottom"===t?"top":"left"===t?"right":"right"===t?"left":void 0}},{key:"regionParameters",value:function(t,e){return{top:{fits:this.fitsInRegion("top",t,e),availables:this.availableRegion("top")},bottom:{fits:this.fitsInRegion("bottom",t,e),availables:this.availableRegion("bottom")},left:{fits:this.fitsInRegion("left",t,e),availables:this.availableRegion("left")},right:{fits:this.fitsInRegion("right",t,e),availables:this.availableRegion("right")}}}},{key:"targetAlignmentOffset",value:function(t){var e=this.targetAlign(t),i=this.targetAlignOffset(t);if("center"===e){if("top"===t||"right"===t)return i;if("bottom"===t||"left"===t)return-1*i}else if("edge"===e){if("top"===t||"right"===t)return-1*i;if("bottom"===t||"left"===t)return i}}},{key:"targetPivot",value:function(t,e){var i=this.targetAlign(t),o=this.targetAlignOffset(t),r=void 0;if("center"===i)"top"===t||"bottom"===t?r=e.left+e.width/2:("left"===t||"right"===t)&&(r=e.top+e.height/2);else if("edge"===i){var n=void 0;"top"===t||"bottom"===t?n=[e.left,e.left+e.width]:("left"===t||"right"===t)&&(n=[e.top,e.top+e.height]);var s=o>=0;"top"===t||"right"===t?r=s?n[1]:n[0]:("bottom"===t||"left"===t)&&(r=s?n[0]:n[1])}return r}},{key:"tailPivot",value:function(t,e,i,o){var r=this.targetPivot(t,e),n=void 0;"top"===t||"bottom"===t?n=r-o.left-i.width/2:("left"===t||"right"===t)&&(n=r-o.top-i.height/2);var s=this.targetAlignmentOffset(t);return n+s}},{key:"rootPivot",value:function(t,e,i){var o=this.targetPivot(t,e),r=this.rootAlign(t),n=this.rootAlignOffset(t),s=void 0,a=void 0;if("center"===r)"top"===t||"bottom"===t?s=o-i.width/2:("left"===t||"right"===t)&&(s=o-i.height/2),"top"===t||"right"===t?a=n:("bottom"===t||"left"===t)&&(a=-1*n);else if("edge"===r){var l=void 0;"top"===t||"bottom"===t?l=[o,o-i.width]:("left"===t||"right"===t)&&(l=[o,o-i.height]);var f=n>=0;"top"===t||"right"===t?s=f?l[1]:l[0]:("bottom"===t||"left"===t)&&(s=f?l[0]:l[1]),"top"===t||"right"===t?a=n:("bottom"===t||"left"===t)&&(a=-1*n)}return s+a+this.targetAlignmentOffset(t)}},{key:"calculatePosition",value:function(t,e){var i=this.state.region,o=this.props.hasTail,r=this.rootDimension(),n={},s=0,a=0;o&&(n=this.tailDimension(i),s=n.width,a=n.height);var l={hidden:!this.availableRegion(i),tail:{hidden:!o}},f=void 0;return"root"===this.props.targetOffsetFrom?f=this.props.targetOffset:"tail"===this.props.targetOffsetFrom&&("top"===i||"bottom"===i?f=a+this.props.targetOffset:("left"===i||"right"===i)&&(f=s+this.props.targetOffset)),"top"===i?(l.top=e.top-r.height-f,l.left=this.rootPivot(i,e,r)):"bottom"===i?(l.top=e.top+e.height+f,l.left=this.rootPivot(i,e,r)):"left"===i?(l.top=this.rootPivot(i,e,r),l.left=e.left-r.width-f):"right"===i&&(l.top=this.rootPivot(i,e,r),l.left=e.left+e.width+f),"top"===i||"bottom"===i?l.leftt.width-this.props.edgeOffset&&(l.left=t.width-r.width-this.props.edgeOffset):("left"===i||"right"===i)&&(l.topt.height-this.props.edgeOffset&&(l.top=t.height-r.height-this.props.edgeOffset)),l.top=Math.round(l.top)+t.scrollTop,l.left=Math.round(l.left)+t.scrollLeft,o&&("top"===i?(l.tail.top=r.height,l.tail.left=this.tailPivot(i,e,n,l)):"bottom"===i?(l.tail.top=-1*a,l.tail.left=this.tailPivot(i,e,n,l)):"left"===i?(l.tail.top=this.tailPivot(i,e,n,l),l.tail.left=r.width):"right"===i&&(l.tail.top=this.tailPivot(i,e,n,l),l.tail.left=-1*s),l.tail.top=Math.round(l.tail.top),l.tail.left=Math.round(l.tail.left),l.tail.width=s,l.tail.height=a,l.tail.type=this.tailType(i)),l}},{key:"calculateRegion",value:function(t,e){var i=this.state.region;this.props.persevere&&(i=this.props.region);var o=this.regionParameters(t,e);"top"!==i||o.top.fits?"bottom"!==i||o.bottom.fits?"left"!==i||o.left.fits?"right"!==i||o.right.fits||(i=this.availableAndFitsIn(["left","top","bottom"],o)):i=this.availableAndFitsIn(["right","top","bottom"],o):i=this.availableAndFitsIn(["top","left","right"],o):i=this.availableAndFitsIn(["bottom","left","right"],o),["top","bottom"].indexOf===i||o.top.fits||o.bottom.fits?["left","right"].indexOf===i||o.left.fits||o.right.fits||(i=this.availableAndFitsIn(["top","bottom"],o)):i=this.availableAndFitsIn(["left","right"],o);var r=void 0;return"top"===i||"bottom"===i?t.width-(e.left+e.width/2)-this.props.edgeOffset1?i-1:0),r=1;i>r;r++)o[r-1]=arguments[r];return o.forEach(function(t){var i=Object.keys(t);i.forEach(function(i){e[i]=t[i]})}),e}}},function(t,e,i){"use strict";function o(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=function(t){var e=t.className,i=t.children,o="flowtip-content "+e;return n["default"].createElement("div",{className:o},i)};var r=i(1),n=o(r)},function(t,e,i){"use strict";function o(t){return t&&t.__esModule?t:{"default":t}}function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function s(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a=Object.assign||function(t){for(var e=1;e", + "license": "MIT", + "bugs": { + "url": "https://github.com/qiushihe/flowtip/issues" + }, + "homepage": "https://github.com/qiushihe/flowtip", + "scripts": { + "dev": "webpack-dev-server --devtool eval --progress --colors --content-base demo --host 0.0.0.0 --history-api-fallback --config webpack.dev.config.babel.js", + "build": "webpack -p --config webpack.config.babel.js", + "lint": "eslint src" + }, + "peerDepenencies": { + "react": "^0.14.3", + "react-dom": "^0.14.3" + }, + "devDependencies": { + "babel-core": "^6.4.0", + "babel-eslint": "^5.0.0-beta6", + "babel-loader": "^6.2.1", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babel-preset-stage-0": "^6.3.13", + "eslint": "^1.10.3", + "eslint-loader": "^1.2.0", + "eslint-plugin-react": "^3.15.0", + "path": "^0.12.7", + "react": "^0.14.3", + "react-dom": "^0.14.3", + "webpack": "^1.12.11", + "webpack-dev-server": "^1.14.1", + "webpack-notifier": "^1.2.1" + } +} diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/demo.js b/src/demo.js new file mode 100644 index 0000000..68fce37 --- /dev/null +++ b/src/demo.js @@ -0,0 +1,127 @@ +/* eslint-disable react/no-multi-comp */ +import React from "react"; +import ReactDOM from "react-dom"; +import FlowTip from "./flowtip"; + +class FlowTipDemo extends React.Component { + state = { + targetProperties: {top: 0, left: 0, width: 0, height: 0}, + parentProperties: {top: 0, left: 0, width: 0, height: 0}, + }; + + updateTargetProperties() { + const target = ReactDOM.findDOMNode(this.refs.target); + const parent = ReactDOM.findDOMNode(this.refs.parent); + + this.setState({ + targetProperties: { + top: target.offsetTop - parent.offsetTop, + left: target.offsetLeft - parent.offsetLeft, + height: target.offsetHeight, + width: target.offsetWidth, + } + }); + } + + updateParentProperties() { + const parent = ReactDOM.findDOMNode(this.refs.parent); + + this.setState({ + parentProperties: { + top: parent.offsetTop, + left: parent.offsetLeft, + height: parent.offsetHeight, + width: parent.offsetWidth, + scrollTop: parent.scrollTop, + scrollLeft: parent.scrollLeft, + } + }); + } + + handleTargetMove(){ + this.updateTargetProperties(); + } + + componentDidMount() { + this.updateParentProperties(); + this.updateTargetProperties(); + } + + render() { + const style = {height: "100%"}; + + const demoAreaStyle = { + width: 800, + height: 500, + marginLeft: 50, + position: "relative" + }; + + const flowtipProperties = { + width: 200, + height: 80, + persevere: false, + targetOffsetFrom: "tail", + targetOffset: 10, + rotationOffset: 15, + edgeOffset: 10, + rootAlign: "center", + rootAlignOffset: 0, + targetAlign: "center", + targetAlignOffset: 0 + }; + + return ( +
+

FlowTip.React Demo

+
+ + Holy Shit! +
+ FlowTip as React Component +
+
+ +
+ ); + } +} + +class FlowTipDemoTarget extends React.Component { + state = { posX: 0, posY: 0 }; + + handleMouseMove(ev) { + const position = { + posX: ev.pageX - 10, + posY: ev.pageY - 10 + }; + + this.setState(position); + this.props.onTargetMove(position); + } + + componentDidMount() { + window.addEventListener("mousemove", this.handleMouseMove.bind(this)); + } + + componentWillUnmount() { + window.removeEventListener("mousemove", this.handleMouseMove.bind(this)); + } + + render() { + const style = { + position: "absolute", + width: 20, + height: 20, + cursor: "default", + top: this.state.posY, + left: this.state.posX + }; + + return ( +
+ ); + } +}; + +ReactDOM.render(, document.getElementById("demo")); diff --git a/src/flowtip-content.js b/src/flowtip-content.js new file mode 100644 index 0000000..202cc71 --- /dev/null +++ b/src/flowtip-content.js @@ -0,0 +1,11 @@ +import React from "react"; + +export default function({ className, children }) { + const classNames = `flowtip-content ${className}`; + + return ( +
+ {children} +
+ ); +}; diff --git a/src/flowtip-root.js b/src/flowtip-root.js new file mode 100644 index 0000000..50ee232 --- /dev/null +++ b/src/flowtip-root.js @@ -0,0 +1,69 @@ +import React from "react"; +import ReactDOM from "react-dom" +import { pick, extend } from "./utils"; + +import FlowtipTail from "./flowtip-tail"; +import FlowtipContent from "./flowtip-content"; + +export default class FlowtopRoot extends React.Component { + static defaultProps = { + width: null, + height: "auto", + minWidth: null, + minHeight: null, + maxWidth: null, + maxHeight: null + }; + + getDimension() { + const root = ReactDOM.findDOMNode(this.refs.root); + + return { + width: root.clientWidth || this.props.width, + height: root.clientHeight || this.props.height + }; + } + + getTailOriginalDimension() { + return this.refs.tail.getOriginalDimension(); + } + + render() { + if (this.props.hidden) { + return null; + } + + const style = { + position: "absolute", + top: this.props.top, + left: this.props.left, + minWidth: this.props.minWidth, + minHeight: this.props.minHeight, + maxWidth: this.props.maxWidth, + maxHeight: this.props.maxHeight, + width: this.props.width, + height: this.props.height + }; + + const classNames = `flowtip-root ${this.props.className}` + + const contentProperties = { + className: this.props.contentClassName + }; + + const tailProperties = extend({ + className: this.props.tailClassName + }, pick(this.props.tail, [ + "top", "left", "width", "height", "hidden", "type" + ])); + + return ( +
+ + {this.props.children} + + +
+ ); + } +}; diff --git a/src/flowtip-tail.js b/src/flowtip-tail.js new file mode 100644 index 0000000..bb3ed53 --- /dev/null +++ b/src/flowtip-tail.js @@ -0,0 +1,39 @@ +import React from "react"; + +export default class FlowtipTail extends React.Component { + static defaultProps = { + width: 20, + height: 10 + }; + + getOriginalDimension() { + return this._originalDimension; + } + + componentWillMount() { + this._originalDimension = { + width: this.props.width, + height: this.props.height + }; + } + + render() { + if (this.props.hidden) { + return null; + } + + const style = { + width: this.props.width, + height: this.props.height, + position: "absolute", + top: this.props.top, + left: this.props.left + }; + + const classNames = `flowtip-tail ${this.props.className}` + + return ( +
+ ); + } +} diff --git a/src/flowtip.js b/src/flowtip.js new file mode 100644 index 0000000..60e71fd --- /dev/null +++ b/src/flowtip.js @@ -0,0 +1,428 @@ +/* eslint-disable complexity */ +import React from "react"; +import { pick, extend } from "./utils"; + +import FlowtipRoot from "./flowtip-root"; + +export default class Flowtip extends React.Component { + static defaultProps = { + className: "", + contentClassName: "", + tailClassName: "", + hasTail: true, + region: "top", + targetOffset: 10, + targetOffsetFrom: "root", + edgeOffset: 30, + rotationOffset: 30, + targetAlign: "center", + targetAlignOffset: 0, + rootAlign: "center", + rootAlignOffset: 0, + persevere: false, + topDisabled: false, + bottomDisabled: false, + leftDisabled: false, + rightDisabled: false, + hideInDisabledRegions: false + }; + + state = { + region: this.props.region, + position: { tail: {} } + }; + + rootAlign(region) { + return this.props[region + "RootAlign"] || this.props.rootAlign; + } + + rootAlignOffset(region) { + return this.props[region + "RootAlignOffset"] || this.props.rootAlignOffset; + } + + targetAlign(region) { + return this.props[region + "TargetAlign"] || this.props.targetAlign; + } + + targetAlignOffset(region) { + return this.props[region + "TargetAlignOffset"] || this.props.targetAlignOffset; + } + + availableRegion(region) { + return !this.props[region + "Disabled"]; + } + + fitsInRegion(region, parent, target) { + const position = this.calculatePosition(parent, target); + const rootDimension = this.rootDimension(); + + if (region === "top") { + return position.top - this.props.edgeOffset >= 0; + } else if (region === "bottom") { + return position.top + rootDimension.height + this.props.edgeOffset <= parent.height; + } else if (region === "left") { + return position.left -this.props.edgeOffset >= 0; + } else if (region === "right") { + return position.left + rootDimension.width + this.props.edgeOffset <= parent.width; + } + } + + availableAndFitsIn(regions, regionParameter, _first) { + if (!_first) { + _first = regions[0]; + } + + const region = regions[0]; + + if (!regions || regions.length <= 0) { + return this.state.region; + } + + const availables = regionParameter[region].availables; + const fits = regionParameter[region].fits; + const hides = this.props.hideInDisabledRegions; + + if ((availables && fits) || (!availables && hides)) { + return region; + } else { + return this.availableAndFitsIn(regions.slice(1), regionParameter, _first); + } + } + + rootDimension() { + return this.refs.root.getDimension(); + } + + tailDimension(region) { + if (!this._tailOriginalDimension) { + this._tailOriginalDimension = this.refs.root.getTailOriginalDimension(); + } + + const dimension = { + width: this._tailOriginalDimension.width, + height: this._tailOriginalDimension.height + }; + + if (region === "left" || region === "right") { + return {width: dimension.height, height: dimension.width}; + } else { + return dimension; + } + } + + tailType(region) { + if (region === "top") { + return "bottom"; + } + + if (region === "bottom") { + return "top"; + } + + if (region === "left") { + return "right"; + } + + if (region === "right") { + return "left"; + } + } + + regionParameters(parent, target) { + return { + top: { + fits: this.fitsInRegion("top", parent, target), + availables: this.availableRegion("top") + }, + bottom: { + fits: this.fitsInRegion("bottom", parent, target), + availables: this.availableRegion("bottom") + }, + left: { + fits: this.fitsInRegion("left", parent, target), + availables: this.availableRegion("left") + }, + right: { + fits: this.fitsInRegion("right", parent, target), + availables: this.availableRegion("right") + } + }; + } + + targetAlignmentOffset(region) { + const targetAlign = this.targetAlign(region); + const targetAlignOffset = this.targetAlignOffset(region); + + if (targetAlign === "center") { + if (region === "top" || region === "right") { + return targetAlignOffset; + } else if (region === "bottom" || region === "left") { + return targetAlignOffset * -1; + } + } else if (targetAlign === "edge") { + if (region === "top" || region === "right") { + return targetAlignOffset * -1; + } else if (region === "bottom" || region === "left") { + return targetAlignOffset; + } + } + } + + targetPivot(region, targetParameter) { + const targetAlign = this.targetAlign(region); + const targetAlignOffset = this.targetAlignOffset(region); + let pivot; + + if (targetAlign === "center") { + if (region === "top" || region === "bottom") { + pivot = targetParameter.left + (targetParameter.width / 2); + } else if (region === "left" || region === "right") { + pivot = targetParameter.top + (targetParameter.height / 2); + } + } else if (targetAlign === "edge") { + let pivots; + if (region === "top" || region === "bottom") { + pivots = [targetParameter.left, targetParameter.left + targetParameter.width]; + } else if (region === "left" || region === "right") { + pivots = [targetParameter.top, targetParameter.top + targetParameter.height]; + } + + const positive = targetAlignOffset >= 0; + if (region === "top" || region === "right") { + pivot = positive ? pivots[1] : pivots[0]; + } else if (region === "bottom" || region === "left") { + pivot = positive ? pivots[0] : pivots[1]; + } + } + + return pivot; + } + + tailPivot(region, targetParameter, tailDimension, rootPosition) { + const targetPivot = this.targetPivot(region, targetParameter); + let pivot; + + if (region === "top" || region === "bottom") { + pivot = targetPivot - rootPosition.left - (tailDimension.width / 2); + } else if (region === "left" || region === "right") { + pivot = targetPivot - rootPosition.top - (tailDimension.height / 2); + } + + const effectiveOffset = this.targetAlignmentOffset(region); + + return pivot + effectiveOffset; + } + + rootPivot(region, targetParameter, rootDimension) { + const targetPivot = this.targetPivot(region, targetParameter); + const rootAlign = this.rootAlign(region); + const rootAlignOffset = this.rootAlignOffset(region); + let pivot; + let effectiveOffset; + + if (rootAlign === "center") { + if (region === "top" || region === "bottom") { + pivot = targetPivot - (rootDimension.width / 2); + } else if (region === "left" || region === "right") { + pivot = targetPivot - (rootDimension.height / 2); + } + + if (region === "top" || region === "right") { + effectiveOffset = rootAlignOffset; + } else if (region === "bottom" || region === "left") { + effectiveOffset = rootAlignOffset * -1; + } + } else if (rootAlign === "edge") { + let pivots; + if (region === "top" || region === "bottom") { + pivots = [targetPivot, targetPivot - rootDimension.width]; + } else if (region === "left" || region === "right") { + pivots = [targetPivot, targetPivot - rootDimension.height]; + } + + const positive = rootAlignOffset >= 0; + if (region === "top" || region === "right") { + pivot = positive ? pivots[1] : pivots[0]; + } else if (region === "bottom" || region === "left") { + pivot = positive ? pivots[0] : pivots[1]; + } + + if (region === "top" || region === "right") { + effectiveOffset = rootAlignOffset; + } else if (region === "bottom" || region === "left") { + effectiveOffset = rootAlignOffset * -1; + } + } + + return pivot + effectiveOffset + this.targetAlignmentOffset(region); + } + + calculatePosition(parent, target) { + const region = this.state.region; + const hasTail = this.props.hasTail; + const rootDimension = this.rootDimension(); + + let tailDimension = {}; + let tailWidth = 0; + let tailHeight = 0; + + if (hasTail) { + tailDimension = this.tailDimension(region); + tailWidth = tailDimension.width; + tailHeight = tailDimension.height; + } + + const position = { + hidden: !this.availableRegion(region), + tail: { + hidden: !hasTail + } + }; + + let effectiveTargetOffset; + + if (this.props.targetOffsetFrom === "root") { + effectiveTargetOffset = this.props.targetOffset; + } else if (this.props.targetOffsetFrom === "tail") { + if (region === "top" || region === "bottom") { + effectiveTargetOffset = tailHeight + this.props.targetOffset; + } else if (region === "left" || region === "right") { + effectiveTargetOffset = tailWidth + this.props.targetOffset + } + } + + if (region === "top") { + position.top = target.top - rootDimension.height - effectiveTargetOffset; + position.left = this.rootPivot(region, target, rootDimension); + } else if (region === "bottom") { + position.top = target.top + target.height + effectiveTargetOffset; + position.left = this.rootPivot(region, target, rootDimension); + } else if (region === "left") { + position.top = this.rootPivot(region, target, rootDimension); + position.left = target.left - rootDimension.width - effectiveTargetOffset; + } else if (region === "right") { + position.top = this.rootPivot(region, target, rootDimension); + position.left = target.left + target.width + effectiveTargetOffset; + } + + if (region === "top" || region === "bottom") { + if (position.left < this.props.edgeOffset) { + position.left = this.props.edgeOffset; + } else if (position.left + rootDimension.width > parent.width - this.props.edgeOffset) { + position.left = parent.width - rootDimension.width - this.props.edgeOffset; + } + } else if (region === "left" || region === "right") { + if (position.top < this.props.edgeOffset) { + position.top = this.props.edgeOffset; + } else if (position.top + rootDimension.height > parent.height - this.props.edgeOffset) { + position.top = parent.height - rootDimension.height - this.props.edgeOffset; + } + } + + position.top = Math.round(position.top) + parent.scrollTop; + position.left = Math.round(position.left) + parent.scrollLeft; + + if (hasTail) { + if (region === "top") { + position.tail.top = rootDimension.height; + position.tail.left = this.tailPivot(region, target, tailDimension, position); + } else if (region === "bottom") { + position.tail.top = tailHeight * -1; + position.tail.left = this.tailPivot(region, target, tailDimension, position); + } else if (region === "left") { + position.tail.top = this.tailPivot(region, target, tailDimension, position); + position.tail.left = rootDimension.width; + } else if (region === "right") { + position.tail.top = this.tailPivot(region, target, tailDimension, position); + position.tail.left = tailWidth * -1; + } + + position.tail.top = Math.round(position.tail.top); + position.tail.left = Math.round(position.tail.left); + position.tail.width = tailWidth; + position.tail.height = tailHeight; + position.tail.type = this.tailType(region); + } + + return position; + } + + calculateRegion(parent, target) { + let region = this.state.region; + + if (this.props.persevere) { + region = this.props.region; + } + + const regionParameter = this.regionParameters(parent, target); + + // Edge detection - flip + if (region === "top" && !regionParameter.top.fits) { + region = this.availableAndFitsIn(["bottom", "left", "right"], regionParameter); + } else if (region === "bottom" && !regionParameter.bottom.fits) { + region = this.availableAndFitsIn(["top", "left", "right"], regionParameter); + } else if (region === "left" && !regionParameter.left.fits) { + region = this.availableAndFitsIn(["right", "top", "bottom"], regionParameter); + } else if (region === "right" && !regionParameter.right.fits) { + region = this.availableAndFitsIn(["left", "top", "bottom"], regionParameter); + } + + // Edge detection - squeeze + if ((["top", "bottom"].indexOf !== region) && !regionParameter.top.fits && !regionParameter.bottom.fits) { + region = this.availableAndFitsIn(["left", "right"], regionParameter); + } else if ((["left", "right"].indexOf !== region) && !regionParameter.left.fits && !regionParameter.right.fits) { + region = this.availableAndFitsIn(["top", "bottom"], regionParameter); + } + + // Edge detection - rotate + let rotateOptions; + if (region === "top" || region === "bottom") { + if ((parent.width) - (target.left + (target.width / 2)) - this.props.edgeOffset < this.props.rotationOffset) { + rotateOptions = region === "top" ? ["left", "bottom"] : ["left", "top"]; + } else if (target.left + (target.width / 2) - this.props.edgeOffset < this.props.rotationOffset) { + rotateOptions = region === "top" ? ["right", "bottom"] : ["right", "top"]; + } + } else if (region === "left" || region === "right") { + if ((parent.height) - (target.top + (target.height / 2)) - this.props.edgeOffset < this.props.rotationOffset) { + rotateOptions = region === "left" ? ["top", "right"] : ["top", "left"]; + } else if (target.top + (target.height / 2) - this.props.edgeOffset < this.props.rotationOffset) { + rotateOptions = region === "left" ? ["bottom", "right"] : ["bottom", "left"]; + } + } + + if (rotateOptions) { + region = this.availableAndFitsIn(rotateOptions, regionParameter); + } + + return region; + } + + componentWillReceiveProps(nextProps) { + const newRegion = this.calculateRegion(nextProps.parent || this.props.parent, nextProps.target || this.props.target); + this.setState({region: newRegion}); + + const newPosition = this.calculatePosition(nextProps.parent || this.props.parent, nextProps.target || this.props.target); + this.setState({position: newPosition}); + } + + render() { + const style = { position: "relative" }; + + const rootProperties = extend(pick(this.props, [ + "className", "contentClassName", "tailClassName", + "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight" + ]), pick(this.state.position, ["top", "left", "tail", "hidden"])); + + + rootProperties.tail.width = rootProperties.tail.width || this.props.tailWidth; + rootProperties.tail.height = rootProperties.tail.height || this.props.tailHeight; + + return ( +
+ + {this.props.children} + +
+ ); + } +}; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..2d0cc33 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,23 @@ +module.exports = { + pick(object, keysArray) { + const reduced = keysArray.reduce(function(acc, val) { + if (object[val] !== undefined) { // eslint-disable-line no-undefined + acc[val] = object[val]; + } + return acc + }, {}); + + return reduced + }, + + extend(destination, ...sources) { + const result = destination; + sources.forEach((source) => { + let keys = Object.keys(source); + keys.forEach((key => { + result[key] = source[key]; + })); + }); + return result; + } +} diff --git a/Makefile b/v1/Makefile similarity index 100% rename from Makefile rename to v1/Makefile diff --git a/v1/README.md b/v1/README.md new file mode 100644 index 0000000..1f3f1ec --- /dev/null +++ b/v1/README.md @@ -0,0 +1,369 @@ +# FlowTip + +*A flexible, adaptable and easy to use tooltip positioning library.* + +* [Website](http://qiushihe.github.io/flowtip) +* [Interactive Demo](http://qiushihe.github.io/flowtip/demo.html) +* [Github Repo](https://github.com/qiushihe/flowtip) + +## Dependencies + +* [jQuery](http://jquery.com) +* [Unerscore.js](http://underscorejs.org) +* [CoffeeScript](http://coffeescript.org) (development only) + +## Glossaries + +The documentation and source of this library refers to several terms that may require +clarification: + +**Tooltip**: An instance of **FlowTip** object. + +**Root**: The **root** of a tooltip refers to the element that contains both the tooltip's +"content" element, and "tail" element. + +**Content (element)**: The container element for the tooltip's content. + +**Tail**: The tail of the tooltip. It's usually visualized as a pointy "nub" of sort. + +**Target**: The target is the thing the tooltip points to. + +**Region**: There are four different regions a tooltip could be in at any given time. The four +regions are "top", "bottom", "left" and "right". Region is described relative to the target of +the tooltip. For example, region "top" means the tooltip would appear above the target, and +region "right" means the tooltip would appear to the right side of the target. + +**Pivot**: The pivot is a imaginary line that is aligned to the target and the tooltip's root is +then in term aligned to the pivot. + +## Basic Usage + +Creates an instance of the **FlowTip** object: + + var tooltip = new FlowTip(); + +An instance of the **FlowTip** can be created with options (see **Attributes** section below): + + var myFlowTip = new FlowTip({ + region: "bottom" + className: "my-tip" + hasTail: false + }) + +At this point, the tooltip is not yet "attached" to a target. The tooltip's target can be set by +calling `setTarget`: + + tooltip.setTarget(tooltipTarget); + +... where `tooltipTarget` may be a DOM object or a jQuery selection. + +To make the tooltip visible, call `show`: + + tooltip.show(); + +This will render the tooltip is it's not already rendered, and position the tooltip in the +appropriate region (see `region` in **Attributes** section below) with proper alignments +(see **Alignments** section below). + +It's important to note that instances of **FlowTip** does **not** automatically re-position +themselves when their targets move. The responsibility of detecting target movement lies outside +the scope of this library. To update the tooltip's position against its target: + + tooltip.reposition(); + +## Attributes + +### `className`: **String** + +_Default value: ""_ + +Additional class name(s) for the container of the tooltip. + +### `contentClassName`: **String** + +_Default value: ""_ + +Additional class name(s) for the tooltip's content. + +### `tailClassName`: **String** + +_Default value: ""_ + +Additional class name(s) for the tooltip's tail. + +### `appendTo`: **Element** + +_Default value: null_ + +The element within which the tooltip will be inserted into. Can be set or updated by calling +`setAppendTo`. Default value is the document's `body` and the tooltip would thus be free to appear +and move anywhere on the page and edge detection will only be performed on the edge of the page. + +If `appendTo` is set to an element then edge detection will be performed on the edge of +the element instead. + +### `tooltipContent`: **String** or **Element** + +_Default value: null_ + +The content of the tooltip. May be specified as (HTML) string or DOM element. Can be set original +updated by calling `setTooltipContent`. + +### `region`: **String** + +_Default value: top_ + +The preferred region in which the tooltip will appear at first relative to its target. Possibly +values are: `top`, `bottom`, `left` and `right`. + +### `topDisabled`, `bottomDisabled`, `leftDisabled` and `rightDisabled`: **Boolean** + +_Default value: false_ + +When set to `true`, the specified region will become unavailable for the various edge detection +algorithms. + +### `hideInDisabledRegions`: **Boolean** + +_Default value: false_ + +When set to `true`, and when the only suitable regions for the tooltip are disabled, the tooltip +will be hidden. When set to `false`, and when the only suitable regions for the tooltip are +disabled, the tooltip will be placed in its original region. + +### `persevere`: **Boolean** + +_Default value: false_ + +If set to `true`, the tooltip will revert back to its preferred region whenever there is enough +space in that region. If set to `false`, the tooltip will remain in the region edge detection +puts it in, until edge detection changes it again. + +### `hasTail`: **Boolean** + +_Default value: true_ + +Controls if the tooltip's tail's visibility. When set to `false` the tooltip's tail will be hidden +and ignored for all positioning calculations. + +### `width`: **Integer** + +_Default value: null_ + +Width of the tooltip's root. If set to `null`, the tooltip will expand to fit its content's width. + +### `height`: **Integer** or the string "auto" + +_Default value: auto_ + +Height of the tooltip's root. If set to `null`, the tooltip will expand to fit its content's +height. If set to `"auto"`, the tooltip's content will be relatively positioned to allow possible +scrolling. + +### `minWidth`: **String** + +_Default value: null_ + +In the form of `42px`, or other forms accepted by CSS min-width. + +### `minHeight`: **String** + +_Default value: null_ + +In the form of `42px`, or other forms accepted by CSS max-width. + +### `maxWidth`: **String** + +_Default value: null_ + +In the form of `42px`, or other forms accepted by CSS min-height. + +### `maxHeight`:**String** + +_Default value: null_ + +In the form of `42px`, or other forms accepted by CSS max-height. + +### `tailWidth`: **Integer** + +_Default value: 20_ + +Width of the tail element. + +### `tailHeight`: **Integer** + +_Default value: 10_ + +Height of the tail element. + +### `targetOffset`: **Integer** + +_Default value: 10_ + +The distance between the tooltip's root (or tip of the tail, see `targetOffsetFrom` below) and +the target's edge. + +### `targetOffsetFrom`: **String** + +_Default value: root_ + +Possible values are `root` and `tail`. When set to `root`, `targetOffset` will be calculated +against the edge of the tooltip's root; When set to `tail`, `targetOffset` will be calculated +against the tip of the tail. + +### `edgeOffset`: **Integer** + +_Default value: 30_ + +The distance between the tooltip's root's edge and the edge of the boundary representative the +tooltip's `appendTo` element. When this distance is smaller than `edgeOffset`, edge detection +will place the tooltip in an opposite region (i.e. flipping the tooltip). + +### `rotationOffset`: **Integer** + +_Default value: 30_ + +The distance between the target's edge and the edge of the boundary representative the tooltip's +`appendTo` element. When this distance is smaller than `rotationOffset`, edge detection will +place the tooltip in an adjacent region (i.e. rotating the tooltip). + +### `targetAlign`: **String** + +_Default value: center_ + +Possible values are `center` and `edge`. When set to `center`, the pivot will be center aligned +against the target. When set to `edge`, one of the pivot's edge will be aligned against the pivot. +See `targetAlignOffset` for the exact mechanics of the two values. This value can also be +specified on a per-region basis via `[top|bottom|left|right]TargetAlign`, and if a region's +specific value is absence, the value from `targetAlign` will be used. + +### `targetAlignOffset`: **Integer** + +_Default value: 0_ + +If `targetAlign` is set to `center`, this value controls the clockwise distance from the center +of the target the pivot will shift. When this value is positive, the pivot will shift clockwise, +and counter-clockwise when this value is negative. + +If `targetAlign` is set to `edge`, the pivot will shift away from one of the target's edges. +When this value is positive, the clockwise edge will be used, and the counter-clockwise edge +will be used when this value is negative. The absolute value of this value controls the amount +of shifting. + +### `rootAlign`: **String** + +_Default value: center_ + +Possible values are `center` and `edge`. When set to `center`, the tooltip's root will be center +aligned against the pivot. When set to `edge`, one of the tooltip's root's edge will be aligned +against the pivot. See `rootAlignOffset` for the exact mechanics of the two values. This value +can also be specified on a per-region basis via `[top|bottom|left|right]RootAlign`, and if a +region's specific value is absence, the value from `rootAlign` will be used. + +### `rootAlignOffset`: **Integer** + +_Default value: 0_ + +If `rootAlign` is set to `center`, this value controls the clockwise distance from the pivot the +tooltip's root will shift. When this value is positive, the root will shift clockwise, and +counter-clockwise when this value is negative. + +If `rootAlign` is set to `edge`, one of the tooltip's root's edge will be aligned against the +pivot. When this value is positive, the clockwise edge will be used, and the counter-clockwise +edge will be used when this value is negative. The absolute value of this value controls the +amount of shifting. + +## Public Methods + +### `constructor` + +Accepts a hash of options. See **Attributes**. + +### `render` + +Render the tooltip's root (if not already rendered), content and insert the tooltip into the DOM. +If called repeatedly, only re-render the tooltip's content. + +### `setAppendTo` + +Set the tooltip's containing element. If the tooltip has already been rendered, the tooltip will +be moved/inserted into the new containing element. + +### `setTarget` + +Set the tooltip's target. The target is the element to which the tooltip will be pointing at. + +### `setClientRectTarget` + +Set the tooltip's target to be a ClientRect instead of a html element. The target is the +ClientRect to which the tooltip will be pointing at. + +### `setTooltipContent` + +Set the tooltip's content. If `render` is set to `true` for `options`, the `render()` method will +be called to re-render the content. + +### `reposition` + +Perform edge detection and position calculations to update the tooltip's position. + +### `show` + +Show the tooltip. Also update the tooltip's `visible` attribute. + +### `hide` + +Hide the tooltip. Also update the tooltip's `visible` attribute. + +### `trigger` + +Trigger an event from the root of the tooltip. + +### `destroy` + +Remove the tooltip's root from DOM. + +## Edge Detection + +There are three strategies for detection: **flip**, **rotate** and **squeeze**. + +### Flip + +The tooltip will be flipped when the root of the tooltip gets too close to the edge of the +boundary. Adjust `edgeOffset` to control the amount of space allowed. The tooltip will be +flipped horizontally when in the left and right regions, and vertically when in the top and +bottom regions. + +### Rotate + +The tooltip will be rotated to an adjacent region if the target gets too close to the edge of +the boundary. Adjust `rotationOffset` to control the amount of space allowed. For example, the +tooltip will be rotated to the top region if the tooltip is currently in the left or right +region, and the target gets too close to the bottom edge of the boundary. + +### Squeeze + +The tooltip will be squeezed to an adjacent region if the root of the tooltip gets too close to +the edge of the boundary on both sides. For example, if the tooltip is in left or right region, +and neither region has enough space, the tooltip will be squeezed to the top region. + +## Alignments + +Tool tip's alignments are divided into **root alignment** and **target alignment**, each with +a corresponding **offset** attribute that controls the direction of the alignment and offset +amount. + +### Target Alignment + +Target alignment refers to the alignment of the pivot relative to the target of the tooltip. See +`targetAlign` and `targetAlignOffset`. + +### Root Alignment + +Root alignment refers to the alignment of the tooltip's root relative to the pivot. See +`rootAlign` and `rootAlignOffset`. + +## Coordinator + +_TODO: Add documentation for `FlowTip.Coordinator`_ + diff --git a/coordinator.coffee b/v1/coordinator.coffee similarity index 100% rename from coordinator.coffee rename to v1/coordinator.coffee diff --git a/flowtip.coffee b/v1/flowtip.coffee similarity index 100% rename from flowtip.coffee rename to v1/flowtip.coffee diff --git a/flowtip.js b/v1/flowtip.js similarity index 100% rename from flowtip.js rename to v1/flowtip.js diff --git a/pages/coordinator-demo.css b/v1/pages/coordinator-demo.css similarity index 100% rename from pages/coordinator-demo.css rename to v1/pages/coordinator-demo.css diff --git a/pages/coordinator-demo.html b/v1/pages/coordinator-demo.html similarity index 100% rename from pages/coordinator-demo.html rename to v1/pages/coordinator-demo.html diff --git a/pages/coordinator-demo.js b/v1/pages/coordinator-demo.js similarity index 100% rename from pages/coordinator-demo.js rename to v1/pages/coordinator-demo.js diff --git a/pages/demo.css b/v1/pages/demo.css similarity index 100% rename from pages/demo.css rename to v1/pages/demo.css diff --git a/pages/demo.html b/v1/pages/demo.html similarity index 100% rename from pages/demo.html rename to v1/pages/demo.html diff --git a/pages/demo.js b/v1/pages/demo.js similarity index 100% rename from pages/demo.js rename to v1/pages/demo.js diff --git a/pages/flowtip.js b/v1/pages/flowtip.js similarity index 100% rename from pages/flowtip.js rename to v1/pages/flowtip.js diff --git a/webpack.config.babel.js b/webpack.config.babel.js new file mode 100644 index 0000000..0290d57 --- /dev/null +++ b/webpack.config.babel.js @@ -0,0 +1,33 @@ +import path from "path"; +import WebpackNotifierPlugin from "webpack-notifier"; + +const config = { + entry: path.resolve(__dirname, "src/flowtip.js"), + output: { + path: path.resolve(__dirname, "lib"), + filename: "flowtip.js", + library: "flowtip", + libraryTarget: "umd" + }, + externals: { + "react": "react", + "react-dom": "react-dom" + }, + resolve: { + alias: {}, + extensions: ["", ".js"] // allow require without extension + }, + plugins: [ + new WebpackNotifierPlugin({ title: "flowtip" }) + ], + module: { + preLoaders: [ + { test: /\.js$/, exclude: /node_modules/, loader: "eslint" } + ], + loaders: [ + { test: /\.js$/, exclude: /node_modules/, loader: "babel" } + ] + } +}; + +export default config; diff --git a/webpack.dev.config.babel.js b/webpack.dev.config.babel.js new file mode 100644 index 0000000..b851313 --- /dev/null +++ b/webpack.dev.config.babel.js @@ -0,0 +1,37 @@ +import path from "path"; +import WebpackNotifierPlugin from "webpack-notifier"; + +const config = { + target: "web", + entry: { + app: [ + "webpack-dev-server/client?http://0.0.0.0:8080",, + "./src/demo.js" + ] + }, + output: { + path: path.join(__dirname, "demo"), + filename: "demo.js", + publicPath: "/", + library: "flowtip", + libraryTarget: "umd" + }, + resolve: { + alias: {}, + extensions: ["", ".js"] // allow require without extension + }, + plugins: [ + new WebpackNotifierPlugin({ title: "flowtip" }) + ], + module: { + preLoaders: [ + { test: /\.js$/, exclude: /node_modules/, loader: "eslint" } + ], + loaders: [ + { test: /\.js$/, exclude: /node_modules/, loader: "babel" } + ] + } +}; + +export default config; +