├── .gitignore
├── .jshintrc
├── .npmignore
├── LICENSE
├── README.md
├── gulpfile.js
├── index.js
├── lib
├── js
│ ├── Annotation.js
│ ├── AnnotatorForm.js
│ ├── Indicator.js
│ └── Mixin.js
└── styles
│ └── annotator.css
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Commenting this out is preferred by some people, see
24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
25 | node_modules
26 |
27 | # Users Environment Variables
28 | .lock-wscript
29 |
30 | # Distro files
31 | dist
32 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "jasmine": true,
4 | "browser": true,
5 | "esnext": true,
6 | "bitwise": true,
7 | "curly": true,
8 | "eqeqeq": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": true,
12 | "noarg": true,
13 | "regexp": true,
14 | "undef": true,
15 | "unused": true,
16 | "strict": true,
17 | "trailing": true,
18 | "smarttabs": true,
19 | "newcap": false
20 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | lib
2 | gulpfile.js
3 | .jshintrc
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Jake Marsh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-annotator [](http://badge.fury.io/js/react-annotator)
2 | =======================================================================================================================
3 |
4 | A React mixin to allow for user annotations directly on elements, similar to [Red Pen](https://redpen.io/). An example can be seen [here](http://jakemmarsh.com/react-annotator/).
5 |
6 | ---
7 |
8 | ### Getting Started
9 |
10 | 1. `npm install --save react-annotator`
11 | 2. `var ReactAnnotatorMixin = require('react-annotator').Mixin`
12 | 3. `mixins: [ReactAnnotatorMixin({settings})]`
13 | 4. call `this.renderAnnotationIndicators()` in the component's `render()` function, in order to render the individual indicators at the top-level of the component element.
14 |
15 | ```javascript
16 | var ReactAnnotatorMixin = require('react-annotator').Mixin;
17 | var annotatorSettings = {
18 | element: '.annotator-target',
19 | annotations: [
20 | {
21 | text: 'This is an annotation on the element.',
22 | xPos: 127,
23 | yPos: 431
24 | },
25 | {
26 | text: 'This is another annotation on the element.',
27 | xPos: 513,
28 | yPos: 289
29 | }
30 | ]
31 | };
32 |
33 | var App = React.createClass({
34 |
35 | mixins: [ReactAnnotatorMixin(annotatorSettings)],
36 |
37 | ...
38 |
39 | render: function() {
40 | return (
41 |
42 |
43 | ...
44 | {this.renderAnnotationIndicators()}
45 |
46 | );
47 | }
48 |
49 | });
50 | ```
51 |
52 | **Note:** Any interactive elements within the parent component must call `event.stopPropagation()` on click to prevent triggering the new annotation form.
53 |
54 | ---
55 |
56 | ### Options
57 |
58 | A Javascript object is passed to the `ReactAnnotatorMixin` to specify options, as well as any previously created or saved annotations (there is also a method to define these asychronously, discussed below.) The options are:
59 |
60 | - `element` (string): the element selector for the parent element which the annotations are intended for. No default value, and the annotation system will not be rendered without a valid element.
61 | - `annotations` (array): the array of annotations to be displayed on the parent element. Defaults to an empty array.
62 | - `addCallback` (function): a function to be called any time a new annotation is entered. The callback is invoked with a single parameter, an object representing the annotation saved (of the format below). Defaults to an empty function.
63 |
64 | Each "annotation" in the array represents one indicator on the parent element, which triggers its textual annotation when triggered. An annotation has the following structure:
65 |
66 | ```json
67 | {
68 | "text": "The tip, comment, note, etc. that was saved for this annotation.",
69 | "xPos": "The X coordinate of the annotation indicator, in relation to the parent element.",
70 | "yPos": "The Y coordinate of the annotation indicator, in relation to the parent element."
71 | }
72 | ```
73 |
74 | ---
75 |
76 | ### Methods
77 |
78 | Upon including the mixin, a handful of functions will be available to your component. Some of these are intended strictly for internal use in the mixin (all prefixed with an underscore), but there are a few that are intended to provide you with more complex options and interactions. These are outlined below.
79 |
80 | ##### `setAnnotations(annotations, cb)`
81 |
82 | This function is intended to provide you with a method to asynchronously define your annotations (if they need to be fetched from a database, etc.) It takes a list of annotations (of the form discussed earlier), along with an optional callback function as parameters. This will completely overwrite any existing annotations, setting `this.state.annotations` equal to the `annotations` parameter. Once the state is updated, the callback function will be invoked.
83 |
84 | ##### `setAddCallback(cb)`
85 |
86 | This function allows you to asynchronously define the callback that will be invoked any time a new annotation is entered (mentioned above under Options). Useful when you need to access `props` or `state` in the callback.
87 |
88 | ##### `addAnnotation(annotation, cb)`
89 |
90 | This function takes an annotation (of the form discussed earlier), along with an optional callback function as parameters. It pushes the new annotation onto the existing list, updates its state, and invokes the callback function.
91 |
92 | ##### `removeAnnotation(annotation, cb)`
93 |
94 | This function takes an annotation (of the form discussed earlier), along with an optional callback function as parameters. It iterates over the current list of annotations, removing any determined to be equal to the annotation passed (using [lodash](https://lodash.com/)'s `_.isEqual` function.) The state is then updated accordingly, and the callback function is invoked.
95 |
96 | ##### `getAnnotations()`
97 |
98 | This returns the list of annotations currently within the mixin's state. This is essentially a wrapper for the value `this.state.annotations`.
99 |
100 | ---
101 |
102 | ### Styling
103 |
104 | Some basic styling is provided in `/dist/css/annotator.css`. This can either be included directly in your project, or used as a base for your own custom styles. Below, the HTML structure of the annotation system is also outlined for custom styling.
105 |
106 | The annotation system consists of two main elements for each annotation: an `indicator` and a `tooltip`. An indicator is a small, simple element positioned absolutely on the parent element. Upon hover, the associated annotation is triggered which the user can then read.
107 |
108 | In addition, when an empty space is clicked on the parent element an annotation `form` is triggered. This allows the user to enter the annotation text to be saved for that point.
109 |
110 | ##### Indicator
111 |
112 | ```html
113 |
114 | ```
115 |
116 | ##### Annotation
117 |
118 | ```html
119 |
122 | ```
123 |
124 | ##### Form
125 |
126 | ```html
127 |
130 | ```
131 |
132 | ---
133 |
134 | ### Testing
135 |
136 | All tests for this package are within the `__tests__/` directory. If you wish to run the tests:
137 |
138 | 1. `git clone git@github.com:jakemmarsh/react-annotator.git`
139 | 2. `cd react-annotator`
140 | 3. `npm install`
141 | 4. `npm test`
142 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var del = require('del');
5 | var react = require('gulp-react');
6 | var runSequence = require('run-sequence');
7 | var stripDebug = require('gulp-strip-debug');
8 | var gulpif = require('gulp-if');
9 |
10 | gulp.task('clean', function(cb) {
11 |
12 | return del(['./dist/css/*', './dist/js/*'], cb);
13 |
14 | });
15 |
16 | gulp.task('styles', function() {
17 |
18 | return gulp.src('./lib/styles/**/*.css')
19 | .pipe(gulp.dest('./dist/css/'));
20 |
21 | });
22 |
23 | gulp.task('scripts', function() {
24 |
25 | return gulp.src('./lib/js/**/*.js')
26 | .pipe(react())
27 | .pipe(gulpif(global.isProd, stripDebug()))
28 | .pipe(gulp.dest('./dist/js/'));
29 |
30 | });
31 |
32 | gulp.task('dev', function() {
33 |
34 | global.isProd = false;
35 |
36 | runSequence(['styles', 'scripts']);
37 |
38 | gulp.watch('./lib/js/**/*.js', ['scripts']);
39 | gulp.watch('./lib/styles/**/*.css', ['styles']);
40 |
41 | });
42 |
43 | gulp.task('prod', ['clean'], function() {
44 |
45 | global.isProd = true;
46 |
47 | return runSequence(['styles', 'scripts']);
48 |
49 | });
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 |
5 | Mixin: require('./dist/js/Mixin'),
6 |
7 | Indicator: require('./dist/js/Indicator'),
8 |
9 | Annotation: require('./dist/js/Annotation'),
10 |
11 | AnnotatorForm: require('./dist/js/AnnotatorForm')
12 |
13 | };
--------------------------------------------------------------------------------
/lib/js/Annotation.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react/addons');
4 |
5 | var Annotation = React.createClass({
6 |
7 | propTypes: {
8 | xPos: React.PropTypes.oneOfType([
9 | React.PropTypes.number,
10 | React.PropTypes.string
11 | ]).isRequired,
12 | yPos: React.PropTypes.oneOfType([
13 | React.PropTypes.number,
14 | React.PropTypes.string
15 | ]).isRequired,
16 | position: React.PropTypes.string.isRequired,
17 | annotation: React.PropTypes.object.isRequired
18 | },
19 |
20 | getDefaultProps: function() {
21 | return {
22 | xPos: -1000,
23 | yPos: -1000,
24 | position: 'bottom',
25 | annotation: {}
26 | };
27 | },
28 |
29 | render: function() {
30 | var classes = 'annotator-tooltip ' + this.props.position;
31 | var styles = {
32 | 'position': 'absolute',
33 | 'top': this.props.yPos,
34 | 'left': this.props.xPos
35 | };
36 |
37 | console.log('visible annotation:', this.props.annotation);
38 |
39 | return (
40 |
41 |
{this.props.annotation.text || ''}
42 |
43 | );
44 | }
45 |
46 | });
47 |
48 | module.exports = Annotation;
--------------------------------------------------------------------------------
/lib/js/AnnotatorForm.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react/addons');
4 |
5 | var AnnotatorForm = React.createClass({
6 |
7 | mixins: [React.addons.LinkedStateMixin],
8 |
9 | propTypes: {
10 | xPos: React.PropTypes.oneOfType([
11 | React.PropTypes.number,
12 | React.PropTypes.string
13 | ]).isRequired,
14 | yPos: React.PropTypes.oneOfType([
15 | React.PropTypes.number,
16 | React.PropTypes.string
17 | ]).isRequired,
18 | saveAnnotation: React.PropTypes.func.isRequired,
19 | closeForm: React.PropTypes.func.isRequired
20 | },
21 |
22 | getInitialState: function() {
23 | return {
24 | text: ''
25 | };
26 | },
27 |
28 | componentDidMount: function() {
29 | this.refs.textarea.getDOMNode().focus();
30 | },
31 |
32 | componentDidUpdate: function(prevProps) {
33 | if ( prevProps.xPos !== this.props.xPos || prevProps.yPos !== this.props.yPos ) {
34 | this.setState({ text: '' });
35 | this.refs.textarea.getDOMNode().focus();
36 | }
37 | },
38 |
39 | handleKeyDown: function(evt) {
40 | var keyCode = evt.keyCode || evt.which;
41 |
42 | if ( keyCode === '27' || keyCode === 27 ) {
43 | this.props.closeForm();
44 | } else if ( keyCode === '13' || keyCode === 13 ) {
45 | evt.preventDefault();
46 | this.props.saveAnnotation(this.state.text);
47 | }
48 | },
49 |
50 | render: function() {
51 | var styles = {
52 | 'position': 'absolute',
53 | 'top': this.props.yPos,
54 | 'left': this.props.xPos
55 | };
56 |
57 | return (
58 |
59 |
64 |
65 | );
66 | }
67 |
68 | });
69 |
70 | module.exports = AnnotatorForm;
--------------------------------------------------------------------------------
/lib/js/Indicator.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react/addons');
4 |
5 | var Indicator = React.createClass({
6 |
7 | propTypes: {
8 | annotation: React.PropTypes.object.isRequired,
9 | showAnnotation: React.PropTypes.func.isRequired,
10 | closeAnnotation: React.PropTypes.func.isRequired
11 | },
12 |
13 | getDefaultProps: function() {
14 | return {
15 | annotation: {
16 | text: '',
17 | xPos: -1000,
18 | yPos: -1000
19 | }
20 | };
21 | },
22 |
23 | stopPropagation: function(evt) {
24 | // Prevent clicks on indicators from triggering new annotation form
25 | evt.preventDefault();
26 | evt.stopPropagation();
27 | },
28 |
29 | showAnnotation: function(evt) {
30 | var element = this.getDOMNode();
31 | var xPos = this.props.annotation.xPos;
32 | var yPos = this.props.annotation.yPos + element.offsetHeight;
33 |
34 | if ( evt ) { evt.preventDefault(); }
35 |
36 | this.props.showAnnotation({
37 | annotation: this.props.annotation,
38 | xPos: xPos,
39 | yPos: yPos
40 | });
41 | },
42 |
43 | render: function() {
44 | var styles = {
45 | 'position': 'absolute',
46 | 'top': this.props.annotation.yPos,
47 | 'left': this.props.annotation.xPos
48 | };
49 |
50 | return (
51 |
57 | );
58 | }
59 |
60 | });
61 |
62 | module.exports = Indicator;
--------------------------------------------------------------------------------
/lib/js/Mixin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react/addons');
4 | var _ = require('lodash');
5 |
6 | var Indicator = require('./Indicator');
7 | var Annotation = require('./Annotation');
8 | var AnnotatorForm = require('./AnnotatorForm');
9 |
10 | module.exports = function(settings) {
11 |
12 | var mixin = {
13 |
14 | _target: null,
15 |
16 | _annotationTarget: null,
17 |
18 | _addCallback: settings.addCallback || function() {},
19 |
20 | getInitialState: function() {
21 | return {
22 | annotations: settings.annotations || [],
23 | visibleAnnotation: null,
24 | annotationXPos: null,
25 | annotationYPos: null,
26 | annotationPosition: null,
27 | addingAnnotation: false,
28 | newAnnotationXPos: null,
29 | newAnnotationYPos: null
30 | };
31 | },
32 |
33 | _renderAnnotatorLayer: function() {
34 | // By calling this method in componentDidMount() and componentDidUpdate(), you're effectively
35 | // creating a "wormhole" that funnels React's hierarchical updates through to a DOM node on an
36 | // entirely different part of the page.
37 | if ( !this._target ) {
38 | this._target = document.createElement('div');
39 | document.body.appendChild(this._target);
40 | }
41 |
42 | React.render(this.renderAnnotationOrForm(), this._target);
43 | },
44 |
45 | _unrenderAnnotatorLayer: function() {
46 | if ( this._target ) {
47 | React.unmountComponentAtNode(this._target);
48 | }
49 | },
50 |
51 | _beginAddProcess: function(evt) {
52 | var xPos = evt.clientX;
53 | var yPos = evt.clientY;
54 |
55 | if ( !this.state.visibleAnnotation ) {
56 | this.setState({
57 | addingAnnotation: true,
58 | newAnnotationXPos: xPos,
59 | newAnnotationYPos: yPos
60 | }, this._renderAnnotatorLayer);
61 | }
62 | },
63 |
64 | _saveAnnotation: function(text) {
65 | var rect = this._annotationTarget.getBoundingClientRect();
66 | var annotation = {
67 | text: text,
68 | xPos: this.state.newAnnotationXPos - rect.left,
69 | yPos: this.state.newAnnotationYPos - rect.top
70 | };
71 |
72 | this.addAnnotation(annotation, function() {
73 | this._addCallback(annotation);
74 | this._closeForm();
75 | }.bind(this));
76 | },
77 |
78 | _calculateAnnotationPosition: function(options) {
79 | var position = null;
80 |
81 | if ( options.yPos + 250 > document.body.offsetHeight ) {
82 | position = 'right';
83 | } else {
84 | position = 'bottom';
85 | }
86 |
87 | return position;
88 | },
89 |
90 | _setVisibleAnnotation: function(options) {
91 | var rect = this._annotationTarget.getBoundingClientRect();
92 |
93 | options.xPos += rect.left;
94 | options.yPos += rect.top;
95 | this.setState({
96 | addingAnnotation: false,
97 | newAnnotationXPos: null,
98 | newAnnotationYPos: null,
99 | visibleAnnotation: options.annotation,
100 | annotationXPos: options.xPos,
101 | annotationYPos: options.yPos,
102 | annotationPosition: this._calculateAnnotationPosition(options)
103 | }, this._renderAnnotatorLayer);
104 | },
105 |
106 | _closeAnnotation: function() {
107 | this.setState({
108 | visibleAnnotation: null,
109 | annotationXPos: null,
110 | annotationYPos: null,
111 | annotationPosition: null
112 | }, this._unrenderAnnotatorLayer);
113 | },
114 |
115 | _closeForm: function() {
116 | this.setState({
117 | addingAnnotation: false,
118 | newAnnotationXPos: null,
119 | newAnnotationYPos: null
120 | }, this._unrenderAnnotatorLayer);
121 | },
122 |
123 | _bindClick: function() {
124 | this._annotationTarget = settings.element ? document.querySelector(settings.element) : null;
125 |
126 | if ( this._annotationTarget ) {
127 | this._annotationTarget.addEventListener('click', this._beginAddProcess, false);
128 | }
129 | },
130 |
131 | _unbindClick: function() {
132 | if ( this._annotationTarget ) {
133 | this._annotationTarget.removeEventListener('click', this._beginAddProcess, false);
134 | }
135 | },
136 |
137 | componentDidMount: function() {
138 | if ( this.state.visibleAnnotation ) {
139 | // Appending to the body is easier than managing the z-index of everything on the page.
140 | // It's also better for accessibility and makes stacking a snap (since components will stack
141 | // in mount order).
142 | this._renderAnnotatorLayer();
143 | }
144 |
145 | this._bindClick();
146 | },
147 |
148 | componentDidUpdate: function() {
149 | this._bindClick();
150 | },
151 |
152 | componentWillUnmount: function() {
153 | var isRendered = this.state.addingAnnotation || this.state.visibleAnnotation;
154 |
155 | if ( this._target && isRendered ) {
156 | this._unrenderAnnotatorLayer();
157 | document.body.removeChild(this._target);
158 | }
159 |
160 | this._unbindClick();
161 | },
162 |
163 | setAnnotations: function(annotations, cb) {
164 | this.setState({
165 | annotations: annotations || []
166 | }, cb);
167 | },
168 |
169 | setAddCallback: function(cb) {
170 | this._addCallback = cb || function() {};
171 | },
172 |
173 | getAnnotations: function() {
174 | return this.state.annotations;
175 | },
176 |
177 | addAnnotation: function(annotation, cb) {
178 | var annotationsCopy = this.state.annotations;
179 |
180 | cb = cb || function() {};
181 |
182 | annotationsCopy.push(annotation);
183 |
184 | this.setState({ annotations: annotationsCopy }, cb);
185 | },
186 |
187 | removeAnnotation: function(annotationToDelete, cb) {
188 | var annotationsCopy = _.reject(this.state.annotations, function(annotation) {
189 | return _.isEqual(annotation, annotationToDelete);
190 | });
191 |
192 | cb = cb || function() {};
193 |
194 | this.setState({ annotations: annotationsCopy }, cb);
195 | },
196 |
197 | renderAnnotationIndicators: function() {
198 | return _.map(this.state.annotations, function(annotation, index) {
199 | return (
200 |
204 | );
205 | }.bind(this));
206 | },
207 |
208 | renderAnnotationOrForm: function() {
209 | var element = null;
210 |
211 | if ( this.state.addingAnnotation ) {
212 | element = (
213 |
217 | );
218 | } else if ( this.state.visibleAnnotation ) {
219 | element = (
220 |
224 | );
225 | }
226 |
227 | return element;
228 | }
229 |
230 | };
231 |
232 | return mixin;
233 |
234 | };
--------------------------------------------------------------------------------
/lib/styles/annotator.css:
--------------------------------------------------------------------------------
1 | .annotator-indicator,
2 | .annotator-tooltip,
3 | .annotator-form {
4 | z-index: 1000;
5 | -webkit-box-sizing: border-box;
6 | -moz-box-sizing: border-box;
7 | box-sizing: border-box;
8 | -webkit-box-shadow: 0px 1px 1.5px rgba(0, 0, 0, 0.5);
9 | -moz-box-shadow: 0px 1px 1.5px rgba(0, 0, 0, 0.5);
10 | box-shadow: 0px 1px 1.5px rgba(0, 0, 0, 0.5);
11 | }
12 |
13 | .annotator-tooltip,
14 | .annotator-form {
15 | padding: 10px;
16 | background-color: #ffffff;
17 | -webkit-border-radius: 6px;
18 | -moz-border-radius: 6px;
19 | -ms-border-radius: 6px;
20 | border-radius: 6px;
21 | }
22 |
23 | .annotator-indicator {
24 | width: 30px;
25 | height: 30px;
26 | background-color: #ff0000;
27 | border: 5px solid #ffffff;
28 | -webkit-border-radius: 100%;
29 | -moz-border-radius: 100%;
30 | -ms-border-radius: 100%;
31 | border-radius: 100%;
32 | }
33 |
34 | .annotator-form {
35 | width: 225px;
36 | }
37 |
38 | .annotator-form > textarea {
39 | width: 100%;
40 | height: 100px;
41 | padding: 5px;
42 | resize: none;
43 | border: 1px dashed #cccccc;
44 | color: #6e7a86;
45 | }
46 |
47 | .annotator-tooltip {
48 |
49 | }
50 |
51 | .annotator-tooltip > p {
52 | margin: 0;
53 | }
54 |
55 | .annotator-tooltip.top {
56 |
57 | }
58 |
59 | .annotator-tooltip.top:after {
60 |
61 | }
62 |
63 | .annotator-tooltip.right {
64 | margin-top: -35px;
65 | margin-left: 45px;
66 | }
67 |
68 | .annotator-tooltip.right:after {
69 | right: 100%;
70 | top: 50%;
71 | border: solid transparent;
72 | content: " ";
73 | height: 0;
74 | width: 0;
75 | position: absolute;
76 | pointer-events: none;
77 | border-right-color: #ffffff; /* same as annotator-tooltip background color */
78 | border-width: 10px;
79 | margin-top: -10px;
80 | }
81 |
82 | .annotator-tooltip.bottom {
83 | margin-top: 15px;
84 | margin-left: -5px;
85 | }
86 |
87 | .annotator-tooltip.bottom:after {
88 | bottom: 100%;
89 | left: 20px;
90 | border: solid transparent;
91 | content: " ";
92 | height: 0;
93 | width: 0;
94 | position: absolute;
95 | pointer-events: none;
96 | border-bottom-color: #fff; /* same as annotator-tooltip background color */
97 | border-width: 10px;
98 | margin-left: -10px;
99 | }
100 |
101 | .annotator-tooltip.left {
102 | margin-top: 15px;
103 | margin-left: -10px;
104 | }
105 |
106 | .annotator-tooltip.left:after {
107 | top: 50%;
108 | left: 100%;
109 | border: solid transparent;
110 | content: " ";
111 | height: 0;
112 | width: 0;
113 | position: absolute;
114 | pointer-events: none;
115 | border-left-color: #ffffff; /* same as annotator-tooltip background color */
116 | border-width: 10px;
117 | margin-top: -10px;
118 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-annotator",
3 | "version": "0.0.1",
4 | "author": "Jake Marsh ",
5 | "description": "A React mixin to allow for user annotations directly on images.",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/jakemmarsh/react-annotator.git"
9 | },
10 | "private": false,
11 | "license": "MIT",
12 | "keywords": [
13 | "ReactJS",
14 | "react",
15 | "image",
16 | "annotate",
17 | "annotation"
18 | ],
19 | "engines": {
20 | "node": ">=0.12.x"
21 | },
22 | "dependencies": {
23 | "lodash": "^3.5.x",
24 | "react": "^0.12.x"
25 | },
26 | "devDependencies": {
27 | "del": "^1.1.1",
28 | "gulp": "^3.8.11",
29 | "gulp-if": "^1.2.5",
30 | "gulp-react": "^3.0.0",
31 | "gulp-strip-debug": "^1.0.2",
32 | "jest-cli": "^0.4.0",
33 | "run-sequence": "^1.0.2"
34 | },
35 | "jest": {
36 | "rootDir": ".",
37 | "testPathDirs": [
38 | ""
39 | ],
40 | "testDirectoryName": "__tests__",
41 | "testFileExtensions": [
42 | "js"
43 | ]
44 | },
45 | "scripts": {
46 | "test": "jest",
47 | "prepublish": "gulp prod"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------