master 3ddde7c7f520 cached
60 files
87.7 KB
23.8k tokens
86 symbols
1 requests
Download .txt
Repository: Secretmapper/react-image-annotation
Branch: master
Commit: 3ddde7c7f520
Files: 60
Total size: 87.7 KB

Directory structure:
gitextract_r5h__py7/

├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── demo/
│   ├── public/
│   │   └── 404.html
│   └── src/
│       ├── App.js
│       ├── components/
│       │   ├── Button/
│       │   │   └── index.js
│       │   ├── Docs/
│       │   │   └── index.js
│       │   ├── Footer/
│       │   │   └── index.js
│       │   ├── GithubStarLink/
│       │   │   └── index.js
│       │   ├── Highlight/
│       │   │   └── index.js
│       │   ├── Home/
│       │   │   ├── index.js
│       │   │   └── simple.txt
│       │   ├── NavBar/
│       │   │   └── index.js
│       │   ├── Root/
│       │   │   └── index.js
│       │   └── Samples/
│       │       ├── Custom/
│       │       │   └── index.js
│       │       ├── Linked/
│       │       │   ├── index.js
│       │       │   └── index.txt
│       │       ├── Multiple/
│       │       │   ├── index.js
│       │       │   └── index.txt
│       │       ├── Simple/
│       │       │   └── index.js
│       │       ├── Threaded/
│       │       │   └── index.js
│       │       └── Touch/
│       │           ├── index.js
│       │           └── index.txt
│       ├── index.css
│       ├── index.html
│       ├── index.js
│       ├── mocks.js
│       └── registerServiceWorker.js
├── nwb.config.js
├── package.json
├── public/
│   └── 404.html
├── src/
│   ├── components/
│   │   ├── Annotation.js
│   │   ├── Content/
│   │   │   └── index.js
│   │   ├── Editor/
│   │   │   └── index.js
│   │   ├── FancyRectangle/
│   │   │   └── index.js
│   │   ├── Oval/
│   │   │   └── index.js
│   │   ├── Overlay/
│   │   │   └── index.js
│   │   ├── Point/
│   │   │   └── index.js
│   │   ├── Rectangle/
│   │   │   └── index.js
│   │   ├── TextEditor/
│   │   │   └── index.js
│   │   └── defaultProps.js
│   ├── hocs/
│   │   ├── OvalSelector.js
│   │   ├── PointSelector.js
│   │   └── RectangleSelector.js
│   ├── index.js
│   ├── selectors.js
│   ├── types/
│   │   └── index.d.ts
│   └── utils/
│       ├── compose.js
│       ├── isMouseHovering.js
│       ├── offsetCoordinates.js
│       └── withRelativeMousePos.js
└── tests/
    ├── .eslintrc
    ├── Annotation.spec.js
    ├── index.test.js
    └── selectors/
        ├── OvalSelector.spec.js
        ├── PointSelector.spec.js
        └── RectangleSelector.spec.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
/coverage
/demo/dist
/es
/lib
/node_modules
/umd
npm-debug.log*
.vscode/

================================================
FILE: .travis.yml
================================================
sudo: false

language: node_js
node_js:
  - 8

before_install:
  - npm install codecov.io coveralls

after_success:
  - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js
  - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js

branches:
  only:
    - master


================================================
FILE: CHANGELOG.md
================================================
## 0.9.8

### Improvements

- Add Type Definitions for Typescript (#12) (thanks @danilofuchs)
- Add support for `children` property (#13) (thanks @federico-bohn)

## 0.9.7

### Fixes

- [Interaction] Fix bug where point annotation would fail abort (#8) (thanks @joshuadeguzman)

## 0.9.6

### Breaking change

- [Interaction] Change annotation click action to click and drag (#6)


================================================
FILE: CONTRIBUTING.md
================================================
## Prerequisites

[Node.js](http://nodejs.org/) >= v4 must be installed.

## Installation

- Running `npm install` in the component's root directory will install everything you need for development.

## Demo Development Server

- `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.

## Running Tests

- `npm test` will run the tests once.

- `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`.

- `npm run test:watch` will run the tests on every change.

## Building

- `npm run build` will build the component for publishing to npm and also bundle the demo app.

- `npm run clean` will delete built resources.


================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)

Copyright (c) 2018-present, Arian Allenson Valdez.

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
React Image Annotation
=========================

An infinitely customizable image annotation library built on React

![Annotation demo](demo.gif)

## Installation

```
npm install --save react-image-annotation
# or
yarn add react-image-annotation
```

## Usage

```js
export default class Simple extends Component {
  state = {
    annotations: [],
    annotation: {}
  }

  onChange = (annotation) => {
    this.setState({ annotation })
  }

  onSubmit = (annotation) => {
    const { geometry, data } = annotation

    this.setState({
      annotation: {},
      annotations: this.state.annotations.concat({
        geometry,
        data: {
          ...data,
          id: Math.random()
        }
      })
    })
  }

  render () {
    return (
      <Root>
        <Annotation
          src={img}
          alt='Two pebbles anthropomorphized holding hands'

          annotations={this.state.annotations}

          type={this.state.type}
          value={this.state.annotation}
          onChange={this.onChange}
          onSubmit={this.onSubmit}
        />
      </Root>
    )
  }
}
```


### Props

Prop | Description | Default
---- | ----------- | -------
`src` | Image src attribute |
`alt` | Image alt attribute |
`annotations` | Array of annotations |
`value` | Annotation object currently being created. See [annotation object](#annotation-object)  |
`onChange` | `onChange` handler for annotation object |
`onSubmit` | `onSubmit` handler for annotation object |
`type` | Selector type. See [custom shapes](#using-custom-shapes) | `RECTANGLE`
`allowTouch` | Set to `true` to allow the target to handle touch events. This disables one-finger scrolling | `false`
`selectors` | An array of selectors. See [adding custom selector logic](#adding-custom-selector-logic) | `[RectangleSelector, PointSelector, OvalSelector]`
`activeAnnotations` | Array of annotations that will be passed as 'active' (active highlight and shows content) |
`activeAnnotationComparator` | Method to compare annotation and `activeAnnotation` item (from `props.activeAnnotations`). Return `true` if it's the annotations are equal | `(a, b) => a === b`
`disableAnnotation` | Set to `true` to disable creating of annotations (note that no callback methods will be called if this is `true`) | `false`
`disableSelector` | Set to `true` to not render `Selector` | `false`
`disableEditor` | Set to `true` to not render `Editor` | `false`
`disableOverlay` | Set to `true` to not render `Overlay` | `false`
`renderSelector` | Function that renders `Selector` Component | See [custom components](#using-custom-components)
`renderEditor` | Function that renders `Editor` Component | See [custom components](#using-custom-components)
`renderHighlight` | Function that renders `Highlight` Component | See [custom components](#using-custom-components)
`renderContent` | Function that renders `Content` | See [custom components](#using-custom-components)
`renderOverlay` | Function that renders `Overlay` | See [custom components](#using-custom-components)
`onMouseUp` | `onMouseUp` handler on annotation target |
`onMouseDown` | `onMouseDown` handler on annotation target |
`onMouseMove` | `onMouseMove` handler on annotation target |
`onClick` | `onClick` handler on annotation target |

#### Annotation object

An Annotation object is an object that conforms to the object shape

```js
({
  selection: T.object, // temporary object for selector logic
  geometry: T.shape({ // geometry data for annotation
    type: T.string.isRequired // type is used to resolve Highlighter/Selector renderer
  }),
  // auxiliary data object for application.
  // Content data can be stored here (text, image, primary key, etc.)
  data: T.object
})
```

## Using custom components

`Annotation` supports `renderProp`s for almost every internal component.

This allows you to customize everything about the the look of the annotation interface, and you can even use canvas elements for performance or more complex interaction models.

- `renderSelector` - used for selecting annotation area (during annotation creation)
- `renderEditor` - appears after annotation area has been selected (during annotation creation)
- `renderHighlight` - used to render current annotations in the annotation interface. It is passed an object that contains the property `active`, which is true if the mouse is hovering over the higlight
- `renderComponent` - auxiliary component that appears when mouse is hovering over the highlight. It is passed an object that contains the annotation being hovered over. `{ annotation }`
- `renderOverlay` - Component overlay for Annotation (i.e. 'Click and Drag to Annotate')

You can view the default renderProps [here](src/components/defaultProps.js)

**Note**: You cannot use `:hover` selectors in css for components returned by `renderSelector` and `renderHighlight`. This is due to the fact that `Annotation` places DOM layers on top of these components, preventing triggering of `:hover`

## Using custom shapes

`Annotation` supports three shapes by default, `RECTANGLE`, `POINT` and `OVAL`.

You can switch the shape selector by passing the appropriate `type` as a property. Default shape `TYPE`s are accessible on their appropriate selectors:

```js
import {
  PointSelector,
  RectangleSelector,
  OvalSelector
} from 'react-image-annotation/lib/selectors'

<Annotation
  type={PointSelector.TYPE}
/>
```

### Adding custom selector logic

#### This is an Advanced Topic

The Annotation API allows support for custom shapes that use custom logic such as polygon or freehand selection. This is done by defining your own selection logic and passing it as a selector in the `selectors` property.

Selectors are objects that must have the following properties:

- `TYPE` - string that uniquely identifies this selector (i.e. `RECTANGLE`)
- `intersects` - method that returns true if the mouse point intersects with the annotation geometry
- `area` - method that calculates and returns the area of the annotation geometry
- `methods` - object that can contain various listener handlers (`onMouseUp`, `onMouseDown`, `onMouseMove`, `onClick`). These listener handlers are called when triggered in the annotation area. These handlers must be reducer-like methods - returning a new annotation object depending on the change of the method

You can view a defined `RectangleSelector` [here](src/hocs/RectangleSelector.js)

### Connecting selector logic to Redux/MobX

First see [Selectors](#adding-custom-selector-logic)

You can use `Selector` methods to connect these method logic to your stores. This is due to the fact that selector methods function as reducers, returning new state depending on the event.

***Note that it is not necessary to connect the selector logic with redux/mobx. Connecting the annotation and annotations state is more than enough for most use cases.***

## License

MIT


================================================
FILE: demo/public/404.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Single Page Apps for GitHub Pages</title>
    <script type="text/javascript">
      // Single Page Apps for GitHub Pages
      // https://github.com/rafrex/spa-github-pages
      // Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
      // ----------------------------------------------------------------------
      // This script takes the current url and converts the path and query
      // string into just a query string, and then redirects the browser
      // to the new url with only a query string and hash fragment,
      // e.g. http://www.foo.tld/one/two?a=b&c=d#qwe, becomes
      // http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe
      // Note: this 404.html file must be at least 512 bytes for it to work
      // with Internet Explorer (it is currently > 512 bytes)
      // If you're creating a Project Pages site and NOT using a custom domain,
      // then set segmentCount to 1 (enterprise users may need to set it to > 1).
      // This way the code will only replace the route part of the path, and not
      // the real directory in which the app resides, for example:
      // https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
      // https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe
      // Otherwise, leave segmentCount as 0.
      var segmentCount = 1;
      var l = window.location;
      l.replace(
        l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
        l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
        l.pathname.slice(1).split('/').slice(segmentCount).join('/').replace(/&/g, '~and~') +
        (l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
        l.hash
      );
    </script>
  </head>
  <body>
  </body>
</html>


================================================
FILE: demo/src/App.js
================================================
import React from 'react'
import { BrowserRouter as Router, Route } from 'react-router-dom'
import styled from 'styled-components'

import NavBar from './components/NavBar'
import Root from './components/Root'
import Home from './components/Home'
import Docs from './components/Docs'
import Footer from './components/Footer'

const Main = styled.main`
  margin: 0 16px;
  margin-top: 51px;
`

export default () => (
  <Router basename='/react-image-annotation'>
    <Root>
      <NavBar
        title='react-image-annotation'
      />
      <Main>
        <Route
          exact
          path='/'
          component={Home}
        />
        <Route
          path='/docs'
          component={Docs}
        />
      </Main>
      <Footer />
    </Root>
  </Router>
)


================================================
FILE: demo/src/components/Button/index.js
================================================
import styled, { css } from 'styled-components'
import { Link } from 'react-router-dom'

const styles = css`
  background: #24B3C8;
  border: 0;
  color: white;
  cursor: pointer;
  font-family: Montserrat;
  font-size: 13px;
  font-weight: 700;
  outline: 0;
  margin: 4px;
  padding: 8px 16px;
  text-shadow: 0 1px 0 rgba(0,0,0,0.1);
  text-transform: uppercase;

  transition: background 0.21s ease-in-out;

  &:focus, &:hover {
    background: #176572;
  }

  ${props => props.active && `
    background: #176572;
  `}
`

export default styled.button`
  ${props => styles}
`

export const ButtonLink = styled(Link)`
  text-decoration: none;
  ${props => styles}
`


================================================
FILE: demo/src/components/Docs/index.js
================================================
import React, { Component } from 'react'
import styled from 'styled-components'
import Highlight from '../Highlight'
import Multi from '../Samples/Multiple'
import multiCode from '../Samples/Multiple/index.txt'
import Linked from '../Samples/Linked'
import linkedCode from '../Samples/Linked/index.txt'
import Custom from '../Samples/Custom'
import Threaded from '../Samples/Threaded'
import Touch from '../Samples/Touch'
import touchCode from '../Samples/Touch/index.txt'

const Container = styled.main`
  margin: 0 auto;
  padding-top: 16px;
  padding-bottom: 64px;
  max-width: 700px;
`

const SourceLink = styled.a`
  display: block;
  margin-top: 8px;
  font-size: 18px;
  text-align: center;
  text-decoration: none;
`

export default class Docs extends Component {
  render () {
    return (
      <Container>
        <h1>Multiple Type/Shape Support</h1>
        <Multi />
        <Highlight>
          {multiCode}
        </Highlight>
        <h1>Controlled Active Annotations</h1>
        <Linked />
        <p>Hover over the text items above and notice how it triggers the active status of their respective annotations</p>
        <Highlight>
          {linkedCode}
        </Highlight>
        <h1>Custom Renderers/Components/Styles</h1>
        <Custom />
        <SourceLink target='_blank' href='https://github.com/Secretmapper/react-image-annotation/blob/master/demo/src/components/Samples/Custom/index.js'>
          View source
        </SourceLink>
        <h1>Threaded Comments (Custom Content Overlay)</h1>
        <Threaded />
        <SourceLink target='_blank' href='https://github.com/Secretmapper/react-image-annotation/blob/master/demo/src/components/Samples/Threaded/index.js'>
          View source
        </SourceLink>
        <h1>Touch support</h1>
        <Touch />
        <Highlight>
          {touchCode}
        </Highlight>
      </Container>
    )
  }
}


================================================
FILE: demo/src/components/Footer/index.js
================================================
import React from 'react'
import styled from 'styled-components'

const Footer = styled.div`
  color: #666;
  padding: 16px;
  padding-bottom: 32px;
  text-align: center;
  a {
    color: inherit;
    text-decoration: none;
    &:hover {
      color: #222;
    }
  }
`

export default () => (
  <Footer>
    <p>
      {'Made with <3 by '}
      <a href='//arianv.com'>@secretmapper</a>
    </p>
  <p>
    Released under the MIT License
  </p>
  </Footer>
)


================================================
FILE: demo/src/components/GithubStarLink/index.js
================================================
import React from 'react'

export default () => (
  <a
    className='github-button'
    href='https://github.com/Secretmapper/react-image-annotation'
    data-size='large'
    data-show-count
    aria-label='Star Secretmapper/react-image-annotation on GitHub'
  >
    Star
  </a>
)


================================================
FILE: demo/src/components/Highlight/index.js
================================================
import React from 'react'
import SyntaxHighlighter from 'react-syntax-highlighter/prism-light'
import prism from 'react-syntax-highlighter/styles/prism/prism'

export default (props) => (
  <SyntaxHighlighter language='jsx' style={prism}>
    {props.children}
  </SyntaxHighlighter>  
)


================================================
FILE: demo/src/components/Home/index.js
================================================
import React, { Component } from 'react'
import styled from 'styled-components'
import Simple from '../Samples/Simple'
import Highlight from '../Highlight'
import GithubStarLink from '../GithubStarLink'
import { ButtonLink } from '../Button'

import simple from './simple.txt'

const Hero = styled.div`
  text-align: center;
`

const Title = styled.h1`
  font-size: 36px;
  text-align: center;
`

const Subtitle = styled.p`
  font-size: 20px;
  text-align: center;
`

const Container = styled.main`
  margin: 0 auto;
  padding: 64px 0;
  max-width: 700px;
`

const GithubButton = styled.div`
  margin-bottom: 16px;
`

export default class App extends Component {
  render () {
    return (
      <Container>
        <Hero>
          <Title>React Image Annotation</Title>
          <Subtitle>
            An infinitely customizable image annotation library built on React
          </Subtitle>
          <GithubButton>
            <GithubStarLink />
          </GithubButton>
          <ButtonLink to='/docs'>
            More Examples
          </ButtonLink>
        </Hero>
        <h2>Install</h2>
        <Highlight>
          npm install --save react-image-annotation
        </Highlight>
        <h2>Demo</h2>
        <Simple />
        <Highlight>
          {simple}
        </Highlight>  
      </Container>
    )
  }
}


================================================
FILE: demo/src/components/Home/simple.txt
================================================
import React, { Component } from 'react'
import Annotation from 'react-image-annotation'

export default class Simple extends Component {
  state = {
    annotations: [],
    annotation: {}
  }

  onChange = (annotation) => {
    this.setState({ annotation })
  }

  onSubmit = (annotation) => {
    const { geometry, data } = annotation

    this.setState({
      annotation: {},
      annotations: this.state.annotations.concat({
        geometry,
        data: {
          ...data,
          id: Math.random()
        }
      })
    })
  }

  render () {
    return (
      <Annotation
        src={IMAGE_URL}
        alt='Two pebbles anthropomorphized holding hands'

        annotations={this.state.annotations}

        type={this.state.type}
        value={this.state.annotation}
        onChange={this.onChange}
        onSubmit={this.onSubmit}
        allowTouch
      />
    )
  }
}


================================================
FILE: demo/src/components/NavBar/index.js
================================================
import React from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'

const Header = styled.header`
  background-color: #fcfcfc;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.25);
  box-sizing: border-box;
  width: 100%;

  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
`

const Items = styled.div`
  margin: 0 auto;
  max-width: 720px;
  display: table;
`

const Item = styled.div`
  display: table-cell;
  padding: 16px 0;
  ${props => props.grow && `
    width: 100%;
  `}

  a {
    color: black;
    text-decoration: none;
    padding: 16px;

    transition:
      background 0.1s ease,
      color 0.21s ease;
    &:hover {
      background: #dadada;
      color: white;
    }
  }
`

const Title = styled(Link)`
  margin-right: 16px;
`

export default (props) => (
  <Header>
    <Items>
      <Item grow>
        <Title to='/'>
          {props.title}
        </Title>
        <Link to='/docs'>Docs</Link>
      </Item>
      <Item>
        <a href='//github.com/Secretmapper/react-image-annotation' target='__blank'>
          Github
        </a>
      </Item>
    </Items>
  </Header>
)


================================================
FILE: demo/src/components/Root/index.js
================================================
import styled from 'styled-components'

export default styled.div`
  font-family: 'Open Sans', sans-serif;
  margin: 0 auto;
  font-size: 14px;

  h1, h2, h3, h4, h5, h6 {
    font-family: 'Montserrat', sans-serif;
  }

  input {
    font-family: 'Open Sans', sans-serif;
  }
`


================================================
FILE: demo/src/components/Samples/Custom/index.js
================================================
import React, { Component } from 'react'
import Annotation from '../../../../../src'
import {
  PointSelector,
  RectangleSelector,
  OvalSelector
} from '../../../../../src/selectors'

import Button from '../../Button'

import mocks from '../../../mocks'
import img from '../../../img.jpeg'

const Box = ({ children, geometry, style }) => (
  <div
    style={{
      ...style,
      position: 'absolute',
      left: `${geometry.x}%`,
      top: `${geometry.y}%`,
      height: `${geometry.height}%`,
      width: `${geometry.width}%`,
    }}
  >
    {children}
  </div>
)

function renderSelector ({ annotation, active }) {
  const { geometry } = annotation
  if (!geometry) return null

  return (
    <Box
      geometry={geometry}
      style={{
        background: 'rgba(255, 255, 255, 0.5)',
        border: 'solid 1px red'
      }}
    >
      Custom Selector
    </Box>
  )
}

function renderHighlight ({ annotation, active }) {
  const { geometry } = annotation
  if (!geometry) return null

  return (
    <Box
      key={annotation.data.id}
      geometry={geometry}
      style={{
        border: 'solid 1px black',
        boxShadow: active
          && '0 0 20px 20px rgba(255, 255, 255, 0.3) inset'
      }}
    >
      Custom Highlight
    </Box>
  )
}

function renderContent ({ annotation }) {
  const { geometry } = annotation
  return (
    <div
      key={annotation.data.id}
      style={{
        background: 'black',
        color: 'white',
        padding: 10,
        position: 'absolute',
        fontSize: 12,
        left: `${geometry.x}%`,
        top: `${geometry.y + geometry.height}%`
      }}
    >
      <div>Custom Content</div>
      {annotation.data && annotation.data.text}
    </div>
  )
}

function renderEditor (props) {
  const { geometry } = props.annotation
  if (!geometry) return null

  return (
    <div
      style={{
        background: 'white',
        borderRadius: 3,
        position: 'absolute',
        left: `${geometry.x}%`,
        top: `${geometry.y + geometry.height}%`,
      }}
    >
      <div>Custom Editor</div>
      <input
        onChange={e => props.onChange({
          ...props.annotation,
          data: {
            ...props.annotation.data,
            text: e.target.value
          }
        })}
      />
      <button onClick={props.onSubmit}>Comment</button>
    </div>
  )
}

function renderOverlay () {
  return (
    <div
      style={{
        background: 'rgba(0, 0, 0, 0.3)',
        color: 'white',
        padding: 5,
        pointerEvents: 'none',
        position: 'absolute',
        top: 5,
        left: 5
      }}
    >
      Custom Overlay
    </div>
  )
}

export default class Custom extends Component {
  state = {
    annotations: [mocks.annotations[0]],
    annotation: {}
  }

  onChange = (annotation) => {
    this.setState({ annotation })
  }

  onSubmit = (annotation) => {
    const { geometry, data } = annotation

    this.setState({
      annotation: {},
      annotations: this.state.annotations.concat({
        geometry,
        data: {
          ...data,
          id: Math.random()
        }
      })
    })
  }

  onChangeType = (e) => {
    this.setState({
      annotation: {},
      type: e.currentTarget.innerHTML
    })
  }

  render () {
    return (
      <div>
        <Annotation
          src={img}
          alt='Two pebbles anthropomorphized holding hands'

          annotations={this.state.annotations}

          type={this.state.type}
          value={this.state.annotation}
          onChange={this.onChange}
          onSubmit={this.onSubmit}
          renderSelector={renderSelector}
          renderEditor={renderEditor}
          renderHighlight={renderHighlight}
          renderContent={renderContent}
          renderOverlay={renderOverlay}
        />
      </div>
    )
  }
}


================================================
FILE: demo/src/components/Samples/Linked/index.js
================================================
import React, { Component } from 'react'
import styled from 'styled-components'
import Annotation from '../../../../../src'

import Root from '../../Root'
import img from '../../../img.jpeg'

const Comments = styled.div`
  border: 1px solid black;
  max-height: 80px;
  overflow: auto;
`

const Comment = styled.div`
  padding: 8px;

  &:nth-child(even) {
    background: rgba(0, 0, 0, .05);
  }
  &:hover {
    background: #ececec;
  }
`

export default class Linked extends Component {
  state = {
    activeAnnotations: [],
    annotations: [
      {
        data: {text: 'Hello!', id: 0.5986265691759928},
        geometry: {type: 'RECTANGLE', x: 25.571428571428573, y: 33, width: 21.142857142857142, height: 34}
      },
      {
        data: {text: 'Hi!', id: 0.5986265691759929},
        geometry: {type: 'RECTANGLE', x: 50.571428571428573, y: 33, width: 21.142857142857142, height: 34}
      }
    ],
    annotation: {}
  }

  onChange = (annotation) => {
    this.setState({ annotation })
  }

  onSubmit = (annotation) => {
    const { geometry, data } = annotation

    this.setState({
      annotation: {},
      annotations: this.state.annotations.concat({
        geometry,
        data: {
          ...data,
          id: Math.random()
        }
      })
    })
  }

  onMouseOver = (id) => e => {
    this.setState({
      activeAnnotations: [
        ...this.state.activeAnnotations,
        id
      ]
    })
  }

  onMouseOut = (id) => e => {
    const index = this.state.activeAnnotations.indexOf(id)

    this.setState({
      activeAnnotations: [
        ...this.state.activeAnnotations.slice(0, index),
        ...this.state.activeAnnotations.slice(index + 1)
      ]
    })
  }

  activeAnnotationComparator = (a, b) => {
    return a.data.id === b
  }

  render () {
    return (
      <Root>
        <Annotation
          src={img}
          alt='Two pebbles anthropomorphized holding hands'

          activeAnnotationComparator={this.activeAnnotationComparator}
          activeAnnotations={this.state.activeAnnotations}
          annotations={this.state.annotations}

          type={this.state.type}
          value={this.state.annotation}
          onChange={this.onChange}
          onSubmit={this.onSubmit}
        />
        <h4>Annotations</h4>
        <Comments>
          {this.state.annotations.map(annotation => (
            <Comment
              onMouseOver={this.onMouseOver(annotation.data.id)}
              onMouseOut={this.onMouseOut(annotation.data.id)}
              key={annotation.data.id}
            >
              {annotation.data.text}
            </Comment>
          ))}
        </Comments>
      </Root>
    )
  }
}


================================================
FILE: demo/src/components/Samples/Linked/index.txt
================================================
classs React extends Component {
  state = {
    activeAnnotations: []
  }

  // ...other React code

  onMouseOver = (id) => e => {
    this.setState({
      activeAnnotations: [
        ...this.state.activeAnnotations,
        id
      ]
    })
  }

  onMouseOut = (id) => e => {
    const index = this.state.activeAnnotations.indexOf(id)

    this.setState({
      activeAnnotations: [
        ...this.state.activeAnnotations.slice(0, index),
        ...this.state.activeAnnotations.slice(index + 1)
      ]
    })
  }

  activeAnnotationComparator = (a, b) => {
    return a.data.id === b
  }

  render () {
    return (
      <Root>
        <Annotation
          activeAnnotationComparator={this.activeAnnotationComparator}
          activeAnnotations={this.state.activeAnnotations}
          {//other props}
        />
        <h4>Annotations</h4>
        <Comments>
          {this.state.annotations.map(annotation => (
            <Comment
              onMouseOver={this.onMouseOver(annotation.data.id)}
              onMouseOut={this.onMouseOut(annotation.data.id)}
              key={annotation.data.id}
            >
              {annotation.data.text}
            </Comment>
          ))}
        </Comments>
      </Root>
    )
  }


================================================
FILE: demo/src/components/Samples/Multiple/index.js
================================================
import React, { Component } from 'react'
import Annotation from '../../../../../src'
import {
  PointSelector,
  RectangleSelector,
  OvalSelector
} from '../../../../../src/selectors'

import Button from '../../Button'

import mocks from '../../../mocks'
import img from '../../../img.jpeg'

export default class Multiple extends Component {
  state = {
    type: RectangleSelector.TYPE,
    annotations: mocks.annotations,
    annotation: {}
  }

  onChange = (annotation) => {
    this.setState({ annotation })
  }

  onSubmit = (annotation) => {
    const { geometry, data } = annotation

    this.setState({
      annotation: {},
      annotations: this.state.annotations.concat({
        geometry,
        data: {
          ...data,
          id: Math.random()
        }
      })
    })
  }

  onChangeType = (e) => {
    this.setState({
      annotation: {},
      type: e.currentTarget.innerHTML
    })
  }

  render () {
    return (
      <div>
        <Button
          onClick={this.onChangeType}
          active={RectangleSelector.TYPE === this.state.type}
        >
          {RectangleSelector.TYPE}
        </Button>
        <Button
          onClick={this.onChangeType}
          active={PointSelector.TYPE === this.state.type}
        >
          {PointSelector.TYPE}
        </Button>
        <Button
          onClick={this.onChangeType}
          active={OvalSelector.TYPE === this.state.type}
        >
          {OvalSelector.TYPE}
        </Button>

        <Annotation
          src={img}
          alt='Two pebbles anthropomorphized holding hands'

          annotations={this.state.annotations}

          type={this.state.type}
          value={this.state.annotation}
          onChange={this.onChange}
          onSubmit={this.onSubmit}
        />
      </div>
    )
  }
}


================================================
FILE: demo/src/components/Samples/Multiple/index.txt
================================================
import React, { Component } from 'react'
import Annotation from 'react-image-annotation'
import {
  PointSelector,
  RectangleSelector,
  OvalSelector
} from 'react-image-annotation/lib/selectors'

export default class Multiple extends Component {
  state = {
    type: RectangleSelector.TYPE,
    annotations: mocks.annotations,
    annotation: {}
  }

  onChange = (annotation) => {
    this.setState({ annotation })
  }

  onSubmit = (annotation) => {
    const { geometry, data } = annotation

    this.setState({
      annotation: {},
      annotations: this.state.annotations.concat({
        geometry,
        data: {
          ...data,
          id: Math.random()
        }
      })
    })
  }

  onChangeType = (e) => {
    this.setState({
      annotation: {},
      type: e.currentTarget.innerHTML
    })
  }

  render () {
    return (
      <div>
        <Button onClick={this.onChangeType}>
          {RectangleSelector.TYPE}
        </Button>
        <Button onClick={this.onChangeType}>
          {PointSelector.TYPE}
        </Button>
        <Button onClick={this.onChangeType}>
          {OvalSelector.TYPE}
        </Button>

        <Annotation
          src={img}
          alt='Two pebbles anthropomorphized holding hands'

          annotations={this.state.annotations}

          type={this.state.type}
          value={this.state.annotation}
          onChange={this.onChange}
          onSubmit={this.onSubmit}
        />
      </div>
    )
  }
}


================================================
FILE: demo/src/components/Samples/Simple/index.js
================================================
import React, { Component } from 'react'
import Annotation from '../../../../../src'

import Root from '../../Root'
import img from '../../../img.jpeg'

export default class Simple extends Component {
  state = {
    annotations: [],
    annotation: {}
  }

  onChange = (annotation) => {
    this.setState({ annotation })
  }

  onSubmit = (annotation) => {
    const { geometry, data } = annotation

    this.setState({
      annotation: {},
      annotations: this.state.annotations.concat({
        geometry,
        data: {
          ...data,
          id: Math.random()
        }
      })
    })
  }

  render () {
    return (
      <Root>
        <Annotation
          src={img}
          alt='Two pebbles anthropomorphized holding hands'

          annotations={this.state.annotations}

          type={this.state.type}
          value={this.state.annotation}
          onChange={this.onChange}
          onSubmit={this.onSubmit}
          allowTouch
        />
      </Root>
    )
  }
}


================================================
FILE: demo/src/components/Samples/Threaded/index.js
================================================
import React, { Component } from 'react'
import Annotation from '../../../../../src'

import styled, { keyframes } from 'styled-components'
import {
  RectangleSelector
} from '../../../../../src/selectors'
import TextEditor from '../../../../../src/components/TextEditor'
import Root from '../../Root'
import img from '../../../img.jpeg'

/*
 * You would normally have the different components here
 * split into different files but I am
 * putting it in one file so it's easier to skim
 */

const Content = styled.div`
  background: white;
  border-radius: 2px;
  box-shadow:
    0px 1px 5px 0px rgba(0, 0, 0, 0.2),
    0px 2px 2px 0px rgba(0, 0, 0, 0.14),
    0px 3px 1px -2px rgba(0, 0, 0, 0.12);
  margin: 8px 0;
`

const ContentClearanceTop = styled.div`
  position: absolute;
  height: 8px;
  top: -8px;
  left: -17px;
  right: -17px;
`

const ContentClearanceLeft = styled.div`
  position: absolute;
  height: 100%;
  left: -17px;
  width: 20px;
`

const ContentClearanceRight = styled.div`
  position: absolute;
  height: 100%;
  right: 0px;
  width: 20px;
`

const fadeInScale = keyframes`
  from {
    opacity: 0;
    transform: scale(0);
  }

  to {
    opacity: 1;
    transform: scale(1);
  }
`

const EditorContainer = styled.div`
  background: white;
  border-radius: 2px;
  box-shadow:
    0px 1px 5px 0px rgba(0, 0, 0, 0.2),
    0px 2px 2px 0px rgba(0, 0, 0, 0.14),
    0px 3px 1px -2px rgba(0, 0, 0, 0.12);
  margin-top: 16px;
  transform-origin: top left;

  animation: ${fadeInScale} 0.31s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  overflow: hidden;
`

const Comment = styled.div`
  border-bottom: 1px solid whitesmoke;
  padding: 8px 16px;
`

const CommentDescription = styled.div`
  margin: 10px 0;
`

const UserPill = styled.span`
  background-color: #2FB3C6;
  border-radius: 4px;
  color: white;
  padding: 2px 4px;
  font-size: 13.5px;
`

class ThreadedEditor extends Component {
  state = { text: '' }

  onUpdateText = (e) => {
    const { props } = this

    // This is the purest (native es6) way to do this
    // You can use a library such as redux and/or
    // lodash/ramda to make this cleaner
    props.onChange({
      ...props.annotation,
      data: {
        ...props.annotation.data,
        comments: [
          this.props.annotation.data
          ? {
            ...this.props.annotation.data.comments[0],
            text: e.target.value
          }
          : {
            id: Math.random(),
            text: e.target.value
          }
        ]
      }
    })
  }

  render () {
    const { props } = this
    const { geometry } = props.annotation
    if (!geometry) return null

    return (
      <EditorContainer
        className={props.className}
        style={{
          position: 'absolute',
          left: `${geometry.x}%`,
          top: `${geometry.y + geometry.height}%`,
          ...props.style
        }}
      >
        <TextEditor
          onChange={this.onUpdateText}
          onSubmit={props.onSubmit}
          value={
            props.annotation.data
              ? props.annotation.data.comments[0].text
              : ''
          }
        />
      </EditorContainer>
    )
  }
}

class ThreadedContent extends Component {
  state = {
    editorText: ''
  }

  onUpdateEditorText = (e) => {
    this.setState({ editorText: e.target.value })
  }

  renderComment (comment) {
    return (
      <Comment key={comment.id}>
        {comment.text}
        <CommentDescription>
          <UserPill>User</UserPill>
        </CommentDescription>
      </Comment>
    )
  }

  render () {
    const { props } = this
    const { annotation } = props
    const { geometry } = annotation
    const comments = annotation.data && annotation.data.comments

    return (
      <React.Fragment>
        <Content
          key={props.annotation.data.id}
          style={{
            position: 'absolute',
            left: `${geometry.x}%`,
            top: `${geometry.y + geometry.height}%`
          }}
        >
          <ContentClearanceTop />
          <ContentClearanceLeft />
          <ContentClearanceRight />
          {(comments) && comments.map(this.renderComment)}
          <TextEditor
            value={this.state.editorText}
            onChange={this.onUpdateEditorText}
            onBlur={props.onBlur}
            onFocus={props.onFocus}
            onSubmit={e => {
              const annotationIndex = props.annotations.indexOf(annotation)
              const annotations = props.annotations.map((annotation, i) => (
                i === annotationIndex
                  ? {
                    ...annotation,
                    data: {
                      ...annotation.data,
                      comments: [
                        ...comments,
                        { id: Math.random(), text: this.state.editorText }
                      ]
                    }
                  }
                  : annotation
              ))

              this.setState({ editorText: '' })
              props.setAnnotations(annotations)
            }}
          />
        </Content>
      </React.Fragment>
    )
  }
}

export default class Threaded extends Component {
  state = {
    activeAnnotations: [],
    annotations: [],
    annotation: {}
  }

  onChange = (annotation) => {
    this.setState({ annotation })
  }

  onSubmit = (annotation) => {
    const { geometry, data } = annotation

    this.setState({
      annotation: {},
      annotations: this.state.annotations.concat({
        geometry,
        data: {
          ...data,
          id: Math.random()
        }
      })
    })
  }

  renderEditor = (props) => {
    const { geometry } = props.annotation
    if (!geometry) return null

    return (
      <ThreadedEditor {...props} />
    )
  }

  renderContent = ({ key, annotation }) => {
    return (
      <ThreadedContent
        key={key}
        annotation={annotation}
        annotations={this.state.annotations}
        setAnnotations={annotations => this.setState({ annotations })}
        onFocus={this.onFocus(key)}
        onBlur={this.onBlur(key)}
      />
    )
  }

  onFocus = (id) => e => {
    this.setState({
      activeAnnotations: [
        ...this.state.activeAnnotations,
        id
      ]
    })
  }

  onBlur = (id) => e => {
    const index = this.state.activeAnnotations.indexOf(id)

    this.setState({
      activeAnnotations: [
        ...this.state.activeAnnotations.slice(0, index),
        ...this.state.activeAnnotations.slice(index + 1)
      ]
    })
  }

  activeAnnotationComparator = (a, b) => {
    return a.data.id === b
  }

  render () {
    return (
      <Root>
        <Annotation
          src={img}
          alt='Two pebbles anthropomorphized holding hands'

          activeAnnotationComparator={this.activeAnnotationComparator}
          activeAnnotations={this.state.activeAnnotations}
          annotations={this.state.annotations}

          type={this.state.type}
          value={this.state.annotation}
          renderEditor={this.renderEditor}
          renderContent={this.renderContent}
          onChange={this.onChange}
          onSubmit={this.onSubmit}
        />
      </Root>
    )
  }
}


================================================
FILE: demo/src/components/Samples/Touch/index.js
================================================
import React, { Component } from 'react'
import Annotation from '../../../../../src'
import {
  PointSelector,
  RectangleSelector,
  OvalSelector
} from '../../../../../src/selectors'

import Button from '../../Button'

import mocks from '../../../mocks'
import img from '../../../img.jpeg'

export default class Multiple extends Component {
  state = {
    type: RectangleSelector.TYPE,
    annotations: mocks.annotations,
    annotation: {},
    allowTouch: true
  }

  onChange = annotation => {
    this.setState({ annotation })
  }

  onSubmit = annotation => {
    const { geometry, data } = annotation

    this.setState({
      annotation: {},
      annotations: this.state.annotations.concat({
        geometry,
        data: {
          ...data,
          id: Math.random()
        }
      })
    })
  }

  onChangeType = e => {
    this.setState({
      annotation: {},
      type: e.currentTarget.innerHTML
    })
  }

  toggleAllowTouch = () => {
    this.setState(prevState => ({ allowTouch: !prevState.allowTouch }))
  }

  render() {
    return (
      <div>
        <div>
          <Button onClick={this.toggleAllowTouch}>
            {this.state.allowTouch
              ? 'Stop allowing touch'
              : 'Start allowing touch'}
          </Button>
        </div>
        <div>
          <Button
            onClick={this.onChangeType}
            active={RectangleSelector.TYPE === this.state.type}
          >
            {RectangleSelector.TYPE}
          </Button>
          <Button
            onClick={this.onChangeType}
            active={PointSelector.TYPE === this.state.type}
          >
            {PointSelector.TYPE}
          </Button>

          <Button
            onClick={this.onChangeType}
            active={OvalSelector.TYPE === this.state.type}
          >
            {OvalSelector.TYPE}
          </Button>
        </div>
        <Annotation
          src={img}
          alt="Two pebbles anthropomorphized holding hands"
          annotations={this.state.annotations}
          allowTouch={this.state.allowTouch}
          type={this.state.type}
          value={this.state.annotation}
          onChange={this.onChange}
          onSubmit={this.onSubmit}
        />
      </div>
    )
  }
}


================================================
FILE: demo/src/components/Samples/Touch/index.txt
================================================
class Touch extends Component {
  state = { allowTouch: true }

  toggleAllowTouch = () => {
    this.setState((prevState) => (
      {allowTouch: !prevState.allowTouch}
    ))
  }

  render () {
    return (
      <div>
        <div>
          <Button onClick={this.toggleAllowTouch}>
            {this.state.allowTouch
              ? "Stop allowing touch"
              : "Start allowing touch"
            }
          </Button>
        </div>
        <Annotation
          src={img}
          alt='Two pebbles anthropomorphized holding hands'
          annotations={this.state.annotations}
          
          allowTouch={this.state.allowTouch}

          type={this.state.type}
          value={this.state.annotation}
          onChange={this.onChange}
          onSubmit={this.onSubmit}
        />
      </div>
    )
  }
}


================================================
FILE: demo/src/index.css
================================================
@import url('https://fonts.googleapis.com/css?family=Montserrat:700|Open+Sans');

html, body {
  margin: 0;
}


================================================
FILE: demo/src/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <!-- Start Single Page Apps for GitHub Pages -->
    <script type="text/javascript">
      // Single Page Apps for GitHub Pages
      // https://github.com/rafrex/spa-github-pages
      // Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
      // ----------------------------------------------------------------------
      // This script checks to see if a redirect is present in the query string
      // and converts it back into the correct url and adds it to the
      // browser's history using window.history.replaceState(...),
      // which won't cause the browser to attempt to load the new url.
      // When the single page app is loaded further down in this file,
      // the correct url will be waiting in the browser's history for
      // the single page app to route accordingly.
      (function(l) {
        if (l.search) {
          var q = {};
          l.search.slice(1).split('&').forEach(function(v) {
            var a = v.split('=');
            q[a[0]] = a.slice(1).join('=').replace(/~and~/g, '&');
          });
          if (q.p !== undefined) {
            window.history.replaceState(null, null,
              l.pathname.slice(0, -1) + (q.p || '') +
              (q.q ? ('?' + q.q) : '') +
              l.hash
            );
          }
        }
      }(window.location))
    </script>
    <!-- End Single Page Apps for GitHub Pages -->
    <script async defer src="https://buttons.github.io/buttons.js"></script>
  </head>
  <body>
    <div id="<%= htmlWebpackPlugin.options.mountId %>"></div>
  </body>
</html>


================================================
FILE: demo/src/index.js
================================================
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import registerServiceWorker from './registerServiceWorker'

import { registerLanguage } from 'react-syntax-highlighter/prism-light'
import jsx from 'react-syntax-highlighter/languages/prism/jsx'

registerLanguage('jsx', jsx)

ReactDOM.render(<App />, document.getElementById('demo'))
registerServiceWorker()


================================================
FILE: demo/src/mocks.js
================================================
import {
  RectangleSelector,
  OvalSelector
} from '../../src/selectors'

export default {
  annotations: [
    {
      geometry:
      {
        type: RectangleSelector.TYPE,
        x: 25,
        y: 31,
        width: 21,
        height: 35
      },
      data: {
        text: 'Annotate!',
        id: 1
      }
    },
    {
      geometry:
      {
        type: OvalSelector.TYPE,
        x: 53,
        y: 33,
        width : 17.5,
        height: 28
      },
      data: {
        text: 'Supports custom shapes too!',
        id: 2
      }
    }
  ]
}


================================================
FILE: demo/src/registerServiceWorker.js
================================================
// In production, we register a service worker to serve assets from local cache.

// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.

// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.

const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
    // [::1] is the IPv6 localhost address.
    window.location.hostname === '[::1]' ||
    // 127.0.0.1/8 is considered localhost for IPv4.
    window.location.hostname.match(
      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
    )
)

export default function register () {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    // The URL constructor is available in all browsers that support SW.
    const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
    if (publicUrl.origin !== window.location.origin) {
      // Our service worker won't work if PUBLIC_URL is on a different origin
      // from what our page is served on. This might happen if a CDN is used to
      // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
      return
    }

    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`

      if (isLocalhost) {
        // This is running on localhost. Lets check if a service worker still exists or not.
        checkValidServiceWorker(swUrl)
      } else {
        // Is not local host. Just register service worker
        registerValidSW(swUrl)
      }
    })
  }
}

function registerValidSW (swUrl) {
  navigator.serviceWorker
    .register(swUrl)
    .then(registration => {
      registration.onupdatefound = () => {
        const installingWorker = registration.installing
        installingWorker.onstatechange = () => {
          if (installingWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              // At this point, the old content will have been purged and
              // the fresh content will have been added to the cache.
              // It's the perfect time to display a "New content is
              // available; please refresh." message in your web app.
              console.log('New content is available; please refresh.')
            } else {
              // At this point, everything has been precached.
              // It's the perfect time to display a
              // "Content is cached for offline use." message.
              console.log('Content is cached for offline use.')
            }
          }
        }
      }
    })
    .catch(error => {
      console.error('Error during service worker registration:', error)
    })
}

function checkValidServiceWorker (swUrl) {
  // Check if the service worker can be found. If it can't reload the page.
  fetch(swUrl)
    .then(response => {
      // Ensure service worker exists, and that we really are getting a JS file.
      if (
        response.status === 404 ||
        response.headers.get('content-type').indexOf('javascript') === -1
      ) {
        // No service worker found. Probably a different app. Reload the page.
        navigator.serviceWorker.ready.then(registration => {
          registration.unregister().then(() => {
            window.location.reload()
          })
        })
      } else {
        // Service worker found. Proceed as normal.
        registerValidSW(swUrl)
      }
    })
    .catch(() => {
      console.log(
        'No internet connection found. App is running in offline mode.'
      )
    })
}

export function unregister () {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(registration => {
      registration.unregister()
    })
  }
}


================================================
FILE: nwb.config.js
================================================
const path = require('path')

module.exports = {
  type: 'react-component',
  npm: {
    esModules: true,
    umd: {
      global: 'ReactImageAnnotation',
      externals: {
        react: 'React'
      }
    }
  },
  webpack: {
    html: {
      template: 'demo/src/index.html'
    },
    extra: {
      module: {
        rules: [
          {test: /\.txt/, loader: 'raw-loader'}
        ]
      }
    }
  },
  karma: {
    testContext: 'tests/index.test.js'
  }
}


================================================
FILE: package.json
================================================
{
  "name": "react-image-annotation",
  "version": "0.9.10",
  "description": "react-image-annotation React component",
  "author": "Arian Allenson Valdez <arianallensonv@gmail.com> (http://arianv.com/)",
  "main": "lib/index.js",
  "module": "es/index.js",
  "files": [
    "css",
    "es",
    "lib",
    "umd"
  ],
  "scripts": {
    "build": "nwb build-react-component",
    "deploy": "gh-pages -d demo/dist",
    "clean": "nwb clean-module && nwb clean-demo",
    "start": "nwb serve-react-demo",
    "test": "nwb test-react",
    "test:coverage": "nwb test-react --coverage",
    "test:watch": "nwb test-react --server"
  },
  "dependencies": {
    "styled-components": "^3.1.6"
  },
  "peerDependencies": {
    "prop-types": "^15.6.0",
    "react": "^16.3",
    "react-dom": ">=0.14"
  },
  "devDependencies": {
    "chai": "^4.1.2",
    "enzyme": "^3.3.0",
    "enzyme-adapter-react-16": "^1.1.1",
    "gh-pages": "^1.1.0",
    "nwb": "0.21.x",
    "raw-loader": "^0.5.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-router": "^4.2.0",
    "react-router-dom": "^4.2.2",
    "react-syntax-highlighter": "^7.0.0",
    "standard": "^10.0.3"
  },
  "standard": {
    "env": [
      "jest",
      "jasmine"
    ],
    "globals": [
      "fetch",
      "URL"
    ],
    "parser": "babel-eslint"
  },
  "homepage": "",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/Secretmapper/react-image-annotation"
  },
  "keywords": [
    "react-component"
  ]
}


================================================
FILE: public/404.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Single Page Apps for GitHub Pages</title>
    <script type="text/javascript">
      // Single Page Apps for GitHub Pages
      // https://github.com/rafrex/spa-github-pages
      // Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
      // ----------------------------------------------------------------------
      // This script takes the current url and converts the path and query
      // string into just a query string, and then redirects the browser
      // to the new url with only a query string and hash fragment,
      // e.g. http://www.foo.tld/one/two?a=b&c=d#qwe, becomes
      // http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe
      // Note: this 404.html file must be at least 512 bytes for it to work
      // with Internet Explorer (it is currently > 512 bytes)
      // If you're creating a Project Pages site and NOT using a custom domain,
      // then set segmentCount to 1 (enterprise users may need to set it to > 1).
      // This way the code will only replace the route part of the path, and not
      // the real directory in which the app resides, for example:
      // https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
      // https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe
      // Otherwise, leave segmentCount as 0.
      var segmentCount = 1;
      var l = window.location;
      l.replace(
        l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
        l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
        l.pathname.slice(1).split('/').slice(segmentCount).join('/').replace(/&/g, '~and~') +
        (l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
        l.hash
      );
    </script>
  </head>
  <body>
  </body>
</html>


================================================
FILE: src/components/Annotation.js
================================================
import React, { Component } from 'react'
import T from 'prop-types'
import styled from 'styled-components'
import compose from '../utils/compose'
import isMouseHovering from '../utils/isMouseHovering'
import withRelativeMousePos from '../utils/withRelativeMousePos'

import defaultProps from './defaultProps'
import Overlay from './Overlay'

const Container = styled.div`
  clear: both;
  position: relative;
  width: 100%;
  &:hover ${Overlay} {
    opacity: 1;
  }
  touch-action: ${(props) => (props.allowTouch ? "pinch-zoom" : "auto")};
`

const Img = styled.img`
  display: block;
  width: 100%;
`

const Items = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
`

const Target = Items

export default compose(
  isMouseHovering(),
  withRelativeMousePos()
)(class Annotation extends Component {
  static propTypes = {
    innerRef: T.func,
    onMouseUp: T.func,
    onMouseDown: T.func,
    onMouseMove: T.func,
    onClick: T.func,
    children: T.object,

    annotations: T.arrayOf(
      T.shape({
        type: T.string
      })
    ).isRequired,
    type: T.string,
    selectors: T.arrayOf(
      T.shape({
        TYPE: T.string,
        intersects: T.func.isRequired,
        area: T.func.isRequired,
        methods: T.object.isRequired
      })
    ).isRequired,

    value: T.shape({
      selection: T.object,
      geometry: T.shape({
        type: T.string.isRequired
      }),
      data: T.object
    }),
    onChange: T.func,
    onSubmit: T.func,

    activeAnnotationComparator: T.func,
    activeAnnotations: T.arrayOf(T.any),

    disableAnnotation: T.bool,
    disableSelector: T.bool,
    renderSelector: T.func,
    disableEditor: T.bool,
    renderEditor: T.func,

    renderHighlight: T.func.isRequired,
    renderContent: T.func.isRequired,

    disableOverlay: T.bool,
    renderOverlay: T.func.isRequired,
    allowTouch: T.bool
  }

  static defaultProps = defaultProps

  targetRef = React.createRef();
  componentDidMount() {
    if (this.props.allowTouch) {
      this.addTargetTouchEventListeners();
    }
  }

  addTargetTouchEventListeners = () => {
    // Safari does not recognize touch-action CSS property,
    // so we need to call preventDefault ourselves to stop touch from scrolling
    // Event handlers must be set via ref to enable e.preventDefault()
    // https://github.com/facebook/react/issues/9809
    
    this.targetRef.current.ontouchstart = this.onTouchStart;
    this.targetRef.current.ontouchend = this.onTouchEnd;
    this.targetRef.current.ontouchmove = this.onTargetTouchMove;
    this.targetRef.current.ontouchcancel = this.onTargetTouchLeave;
    
  }
  removeTargetTouchEventListeners = () => {
    this.targetRef.current.ontouchstart = undefined;
    this.targetRef.current.ontouchend = undefined;
    this.targetRef.current.ontouchmove = undefined;
    this.targetRef.current.ontouchcancel = undefined;
  }

  componentDidUpdate(prevProps) {
    if (this.props.allowTouch !== prevProps.allowTouch) {
      if (this.props.allowTouch) {
        this.addTargetTouchEventListeners()
      } else {
        this.removeTargetTouchEventListeners()
      }
    }
  }

  setInnerRef = (el) => {
    this.container = el
    this.props.relativeMousePos.innerRef(el)
    this.props.innerRef(el)
  }

  getSelectorByType = (type) => {
    return this.props.selectors.find(s => s.TYPE === type)
  }

  getTopAnnotationAt = (x, y) => {
    const { annotations } = this.props
    const { container, getSelectorByType } = this

    if (!container) return

    const intersections = annotations
      .map(annotation => {
        const { geometry } = annotation
        const selector = getSelectorByType(geometry.type)

        return selector.intersects({ x, y }, geometry, container)
          ? annotation
          : false
      })
      .filter(a => !!a)
      .sort((a, b) => {
        const aSelector = getSelectorByType(a.geometry.type)
        const bSelector = getSelectorByType(b.geometry.type)

        return aSelector.area(a.geometry, container) - bSelector.area(b.geometry, container)
      })

    return intersections[0]
  }

  onTargetMouseMove = (e) => {
    this.props.relativeMousePos.onMouseMove(e)
    this.onMouseMove(e)
  }
  onTargetTouchMove = (e) => {
    this.props.relativeMousePos.onTouchMove(e)
    this.onTouchMove(e)
  }

  onTargetMouseLeave = (e) => {
    this.props.relativeMousePos.onMouseLeave(e)
  }
  onTargetTouchLeave = (e) => {
    this.props.relativeMousePos.onTouchLeave(e)
  }

  onMouseUp = (e) => this.callSelectorMethod('onMouseUp', e)
  onMouseDown = (e) => this.callSelectorMethod('onMouseDown', e)
  onMouseMove = (e) => this.callSelectorMethod('onMouseMove', e)
  onTouchStart = (e) => this.callSelectorMethod("onTouchStart", e)
  onTouchEnd = (e) => this.callSelectorMethod("onTouchEnd", e)
  onTouchMove = (e) => this.callSelectorMethod("onTouchMove", e)
  onClick = (e) => this.callSelectorMethod('onClick', e)

  onSubmit = () => {
    this.props.onSubmit(this.props.value)
  }

  callSelectorMethod = (methodName, e) => {
    if (this.props.disableAnnotation) {
      return
    }

    if (!!this.props[methodName]) {
      this.props[methodName](e)
    } else {
      const selector = this.getSelectorByType(this.props.type)
      if (selector && selector.methods[methodName]) {
        const value = selector.methods[methodName](this.props.value, e)

        if (typeof value === 'undefined') {
          if (process.env.NODE_ENV !== 'production') {
            console.error(`
              ${methodName} of selector type ${this.props.type} returned undefined.
              Make sure to explicitly return the previous state
            `)
          }
        } else {
          this.props.onChange(value)
        }
      }
    }
  }

  shouldAnnotationBeActive = (annotation, top) => {
    if (this.props.activeAnnotations) {
      const isActive = !!this.props.activeAnnotations.find(active => (
        this.props.activeAnnotationComparator(annotation, active)
      ))

      return isActive || top === annotation
    } else {
      return top === annotation
    }
  }

  

  render () {
    const { props } = this
    const {
      isMouseHovering,

      renderHighlight,
      renderContent,
      renderSelector,
      renderEditor,
      renderOverlay,
      allowTouch
    } = props

    const topAnnotationAtMouse = this.getTopAnnotationAt(
      this.props.relativeMousePos.x,
      this.props.relativeMousePos.y
    )

    return (
      <Container
        style={props.style}
        innerRef={isMouseHovering.innerRef}
        onMouseLeave={this.onTargetMouseLeave}
        onTouchCancel={this.onTargetTouchLeave}
        allowTouch={allowTouch}
      >
        <Img
          className={props.className}
          style={props.style}
          alt={props.alt}
          src={props.src}
          draggable={false}
          innerRef={this.setInnerRef}
        />
        <Items>
          {props.annotations.map(annotation => (
            renderHighlight({
              key: annotation.data.id,
              annotation,
              active: this.shouldAnnotationBeActive(annotation, topAnnotationAtMouse)
            })
          ))}
          {!props.disableSelector
            && props.value
            && props.value.geometry
            && (
              renderSelector({
                annotation: props.value
              })
            )
          }
        </Items>
        <Target
          innerRef={this.targetRef}
          onClick={this.onClick}
          onMouseUp={this.onMouseUp}
          onMouseDown={this.onMouseDown}
          onMouseMove={this.onTargetMouseMove}
        />
        {!props.disableOverlay && (
          renderOverlay({
            type: props.type,
            annotation: props.value
          })
        )}
        {props.annotations.map(annotation => (
          this.shouldAnnotationBeActive(annotation, topAnnotationAtMouse)
          && (
            renderContent({
              key: annotation.data.id,
              annotation: annotation
            })
          )
        ))}
        {!props.disableEditor
          && props.value
          && props.value.selection
          && props.value.selection.showEditor
          && (
            renderEditor({
              annotation: props.value,
              onChange: props.onChange,
              onSubmit: this.onSubmit
            })
          )
        }
        <div>{props.children}</div>
      </Container>
    )
  }
})


================================================
FILE: src/components/Content/index.js
================================================
import React from 'react'
import styled from 'styled-components'

const Container = styled.div`
  background: white;
  border-radius: 2px;
  box-shadow:
    0px 1px 5px 0px rgba(0, 0, 0, 0.2),
    0px 2px 2px 0px rgba(0, 0, 0, 0.14),
    0px 3px 1px -2px rgba(0, 0, 0, 0.12);
  padding: 8px 16px;
  margin-top: 8px;
  margin-left: 8px;
`

function Content (props) {
  const { geometry } = props.annotation
  if (!geometry) return null

  return (
    <Container
      style={{
        position: 'absolute',
        left: `${geometry.x}%`,
        top: `${geometry.y + geometry.height}%`,
        ...props.style
      }}
      className={props.className}
      geometry={geometry}
    >
      {props.annotation.data && props.annotation.data.text}
    </Container>
  )
}

Content.defaultProps = {
  style: {},
  className: ''
}

export default Content


================================================
FILE: src/components/Editor/index.js
================================================
import React from 'react'
import styled, { keyframes } from 'styled-components'
import TextEditor from '../TextEditor'

const fadeInScale = keyframes`
  from {
    opacity: 0;
    transform: scale(0);
  }

  to {
    opacity: 1;
    transform: scale(1);
  }
`

const Container = styled.div`
  background: white;
  border-radius: 2px;
  box-shadow:
    0px 1px 5px 0px rgba(0, 0, 0, 0.2),
    0px 2px 2px 0px rgba(0, 0, 0, 0.14),
    0px 3px 1px -2px rgba(0, 0, 0, 0.12);
  margin-top: 16px;
  transform-origin: top left;

  animation: ${fadeInScale} 0.31s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  overflow: hidden;
`

function Editor (props) {
  const { geometry } = props.annotation
  if (!geometry) return null

  return (
    <Container
      className={props.className}
      style={{
        position: 'absolute',
        left: `${geometry.x}%`,
        top: `${geometry.y + geometry.height}%`,
        ...props.style
      }}
    >
      <TextEditor
        onChange={e => props.onChange({
          ...props.annotation,
          data: {
            ...props.annotation.data,
            text: e.target.value
          }
        })}
        onSubmit={props.onSubmit}
        value={props.annotation.data && props.annotation.data.text}
      />
    </Container>
  )
}

Editor.defaultProps = {
  className: '',
  style: {}
}

export default Editor


================================================
FILE: src/components/FancyRectangle/index.js
================================================
import React from 'react'
import styled from 'styled-components'

const Box = styled.div`
  background: rgba(0, 0, 0, 0.2);
  position: absolute;
`

const Container = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
`

function FancyRectangle (props) {
  const { geometry } = props.annotation

  if (!geometry) return null

  return (
    <Container
      className={props.className}
      style={props.style}
    >
      <Box
        style={{
          height: `${geometry.y}%`,
          width: '100%'
        }}
      />
      <Box
        style={{
          top: `${geometry.y}%`,
          height: `${geometry.height}%`,
          width: `${geometry.x}%`
        }}
      />
      <Box
        style={{
          top: `${geometry.y}%`,
          left: `${geometry.x + geometry.width}%`,
          height: `${geometry.height}%`,
          width: `${100 - (geometry.x + geometry.width)}%`
        }}
      />
      <Box
        style={{
          top: `${geometry.y + geometry.height}%`,
          height: `${100 - (geometry.y + geometry.height)}%`,
          width: '100%'
        }}
      />
    </Container>
  )
}

FancyRectangle.defaultProps = {
  className: '',
  style: {}
}

export default FancyRectangle


================================================
FILE: src/components/Oval/index.js
================================================
import React from 'react'
import styled from 'styled-components'

const Container = styled.div`
  border: dashed 2px black;
  border-radius: 100%;
  box-shadow: 0px 0px 1px 1px white inset;
  box-sizing: border-box;
  transition: box-shadow 0.21s ease-in-out;
`

function Oval (props) {
  const { geometry } = props.annotation
  if (!geometry) return null

  return (
    <Container
      className={props.className}
      style={{
        position: 'absolute',
        left: `${geometry.x}%`,
        top: `${geometry.y}%`,
        height: `${geometry.height}%`,
        width: `${geometry.width}%`,
        boxShadow: props.active && '0 0 1px 1px yellow inset',
        ...props.style
      }}
    />
  )
}

Oval.defaultProps = {
  className: '',
  style: {}
}

export default Oval


================================================
FILE: src/components/Overlay/index.js
================================================
import React from 'react'
import styled from 'styled-components'

export default styled.div`
  background: rgba(0, 0, 0, .4);
  border-radius: 5px;
  bottom: 4px;
  color: white;
  font-size: 12px;
  font-weight: bold;
  opacity: 0;
  padding: 10px;
  pointer-events: none;
  position: absolute;
  right: 4px;
  transition: opacity 0.21s ease-in-out;
  user-select: none;
`


================================================
FILE: src/components/Point/index.js
================================================
import React from 'react'
import styled from 'styled-components'

const Container = styled.div`
  border: solid 3px white;
  border-radius: 50%;
  box-sizing: border-box;
  box-shadow:
    0 0 0 1px rgba(0,0,0,0.3),
    0 0 0 2px rgba(0,0,0,0.2),
    0 5px 4px rgba(0,0,0,0.4);
  height: 16px;
  position: absolute;
  transform: translate3d(-50%, -50%, 0);
  width: 16px;
` 

function Point (props) {
  const { geometry } = props.annotation
  if (!geometry) return null

  return (
    <Container
      style={{
        top: `${geometry.y}%`,
        left: `${geometry.x}%`
      }}
    />
  )
}


export default Point


================================================
FILE: src/components/Rectangle/index.js
================================================
import React from 'react'
import styled from 'styled-components'

const Container = styled.div`
  border: dashed 2px black;
  box-shadow: 0px 0px 1px 1px white inset;
  box-sizing: border-box;
  transition: box-shadow 0.21s ease-in-out;
`

function Rectangle (props) {
  const { geometry } = props.annotation
  if (!geometry) return null

  return (
    <Container
      className={props.className}
      style={{
        position: 'absolute',
        left: `${geometry.x}%`,
        top: `${geometry.y}%`,
        height: `${geometry.height}%`,
        width: `${geometry.width}%`,
        boxShadow: props.active && '0 0 1px 1px yellow inset',
        ...props.style
      }}
    />
  )
}

Rectangle.defaultProps = {
  className: '',
  style: {}
}

export default Rectangle


================================================
FILE: src/components/TextEditor/index.js
================================================
import React from 'react'
import styled, { keyframes } from 'styled-components'

const Inner = styled.div`
  padding: 8px 16px;

  textarea {
    border: 0;
    font-size: 14px;
    margin: 6px 0;
    min-height: 60px;
    outline: 0;
  }
`

const Button = styled.div`
  background: whitesmoke;
  border: 0;
  box-sizing: border-box;
  color: #363636;
  cursor: pointer;
  font-size: 1rem;
  margin: 0;
  outline: 0;
  padding: 8px 16px;
  text-align: center;
  text-shadow: 0 1px 0 rgba(0,0,0,0.1);
  width: 100%;

  transition: background 0.21s ease-in-out;

  &:focus, &:hover {
    background: #eeeeee;
  }
`

function TextEditor (props) {
  return (
    <React.Fragment>
      <Inner>
        <textarea
          placeholder='Write description'
          onFocus={props.onFocus}
          onBlur={props.onBlur}
          onChange={props.onChange}
          value={props.value}
        >
        </textarea>
      </Inner>
      {props.value && (
        <Button
          onClick={props.onSubmit}
        >
          Submit
        </Button>
      )}
    </React.Fragment>
  )
}

export default TextEditor


================================================
FILE: src/components/defaultProps.js
================================================
import React from 'react'

import Point from './Point'
import Editor from './Editor'
import FancyRectangle from './FancyRectangle'
import Rectangle from './Rectangle'
import Oval from './Oval'
import Content from './Content'
import Overlay from './Overlay'

import {
  RectangleSelector,
  PointSelector,
  OvalSelector
} from '../selectors'

export default {
  innerRef: () => {},
  onChange: () => {},
  onSubmit: () => {},
  type: RectangleSelector.TYPE,
  selectors: [
    RectangleSelector,
    PointSelector,
    OvalSelector
  ],
  disableAnnotation: false,
  disableSelector: false,
  disableEditor: false,
  disableOverlay: false,
  activeAnnotationComparator: (a, b) => a === b,
  renderSelector: ({ annotation }) => {
    switch (annotation.geometry.type) {
      case RectangleSelector.TYPE:
        return (
          <FancyRectangle
            annotation={annotation}
          />
        )
      case PointSelector.TYPE:
        return (
          <Point
            annotation={annotation}
          />
        )
      case OvalSelector.TYPE:
        return (
          <Oval
            annotation={annotation}
          />
        )
      default:
        return null
    }
  },
  renderEditor: ({ annotation, onChange, onSubmit }) => (
    <Editor
      annotation={annotation}
      onChange={onChange}
      onSubmit={onSubmit}
    />
  ),
  renderHighlight: ({ key, annotation, active }) => {
    switch (annotation.geometry.type) {
      case RectangleSelector.TYPE:
        return (
          <Rectangle
            key={key}
            annotation={annotation}
            active={active}
          />
        )
      case PointSelector.TYPE:
        return (
          <Point
            key={key}
            annotation={annotation}
            active={active}
          />
        )
      case OvalSelector.TYPE:
        return (
          <Oval
            key={key}
            annotation={annotation}
            active={active}
          />
        )
      default:
        return null
    }
  },
  renderContent: ({ key, annotation }) => (
    <Content
      key={key}
      annotation={annotation}
    />
  ),
  renderOverlay: ({ type, annotation }) => {
    switch (type) {
      case PointSelector.TYPE:
        return (
          <Overlay>
            Click to Annotate
          </Overlay>
        )
      default:
        return (
          <Overlay>
            Click and Drag to Annotate
          </Overlay>
        )
    }
  }
}


================================================
FILE: src/hocs/OvalSelector.js
================================================
import { getCoordPercentage } from '../utils/offsetCoordinates';

const square = n => Math.pow(n, 2)

export const TYPE = 'OVAL'

export function intersects({ x, y }, geometry) {
  const rx = geometry.width / 2
  const ry = geometry.height / 2
  const h = geometry.x + rx
  const k = geometry.y + ry

  const value = square(x - h) / square(rx) + square(y - k) / square(ry)

  return value <= 1
}

export function area(geometry) {
  const rx = geometry.width / 2
  const ry = geometry.height / 2

  return Math.PI * rx * ry
}

export const methods = {
  onTouchStart(annotation, e) {
    return pointerDown(annotation, e)
  },
  onTouchEnd(annotation, e) {
    return pointerUp(annotation, e)
  },
  onTouchMove(annotation, e) {
    return pointerMove(annotation, e)
  },
  onMouseDown(annotation, e) {
    return pointerDown(annotation, e)
  },
  onMouseUp(annotation, e) {
    return pointerUp(annotation, e)
  },
  onMouseMove(annotation, e) {
    return pointerMove(annotation, e)
  }
}

function pointerDown(annotation, e) {
  if (!annotation.selection) {
    const { x: anchorX, y: anchorY } = getCoordPercentage(e)

    return {
      ...annotation,
      selection: {
        ...annotation.selection,
        mode: 'SELECTING',
        anchorX,
        anchorY
      }
    }
  } else {
    return {}
  }
  return annotation
}

function pointerUp(annotation, e) {
  if (annotation.selection) {
    const { selection, geometry } = annotation

    if (!geometry) {
      return {}
    }

    switch (annotation.selection.mode) {
      case 'SELECTING':
        return {
          ...annotation,
          selection: {
            ...annotation.selection,
            showEditor: true,
            mode: 'EDITING'
          }
        }
      default:
        break
    }
  }
  return annotation
}

function pointerMove(annotation, e) {
  if (annotation.selection && annotation.selection.mode === 'SELECTING') {
    const { anchorX, anchorY } = annotation.selection
    const { x: newX, y: newY } = getCoordPercentage(e)
    const width = newX - anchorX
    const height = newY - anchorY

    return {
      ...annotation,
      geometry: {
        ...annotation.geometry,
        type: TYPE,
        x: width > 0 ? anchorX : newX,
        y: height > 0 ? anchorY : newY,
        width: Math.abs(width),
        height: Math.abs(height)
      }
    }
  }
  return annotation
}

export default {
  TYPE,
  intersects,
  area,
  methods
}


================================================
FILE: src/hocs/PointSelector.js
================================================
import { getCoordPercentage } from '../utils/offsetCoordinates';
const MARGIN = 6

const marginToPercentage = (container) => ({
  marginX: MARGIN / container.width * 100,
  marginY: MARGIN / container.height * 100
})

export const TYPE = 'POINT'

export function intersects ({ x, y }, geometry, container) {
  const { marginX, marginY } = marginToPercentage(container)

  if (x < geometry.x - marginX) return false
  if (y < geometry.y - marginY) return false
  if (x > geometry.x + marginX) return false
  if (y > geometry.y + marginY) return false

  return true
}

export function area (geometry, container) {
  const { marginX, marginY } = marginToPercentage(container)

  return marginX * marginY
}

export const methods = {
  onClick (annotation, e) {
    if (!annotation.geometry) {
      return {
        ...annotation,
        selection: {
          ...annotation.selection,
          showEditor: true,
          mode: 'EDITING'
        },
        geometry: {
          ...annotation.geometry,
          ...getCoordPercentage(e),
          width: 0,
          height: 0,
          type: TYPE,
        }
      }
    } else{
      return {}
    }
  }
}

export default {
  TYPE,
  intersects,
  area,
  methods
}


================================================
FILE: src/hocs/RectangleSelector.js
================================================
import { getCoordPercentage } from '../utils/offsetCoordinates';

export const TYPE = 'RECTANGLE'

export function intersects({ x, y }, geometry) {
  if (x < geometry.x) return false
  if (y < geometry.y) return false
  if (x > geometry.x + geometry.width) return false
  if (y > geometry.y + geometry.height) return false

  return true
}

export function area(geometry) {
  return geometry.height * geometry.width
}

export const methods = {
  onTouchStart(annotation, e) {
    return pointerDown(annotation, e)
  },
  onTouchEnd(annotation, e) {
    return pointerUp(annotation, e)
  },
  onTouchMove(annotation, e) {
    return pointerMove(annotation, e)
  },
  onMouseDown(annotation, e) {
    return pointerDown(annotation, e)
  },
  onMouseUp(annotation, e) {
    return pointerUp(annotation, e)
  },
  onMouseMove(annotation, e) {
    return pointerMove(annotation, e)
  }
}

function pointerDown(annotation, e) {
  if (!annotation.selection) {
    const { x: anchorX, y: anchorY } = getCoordPercentage(e)
    return {
      ...annotation,
      selection: {
        ...annotation.selection,
        mode: 'SELECTING',
        anchorX,
        anchorY
      }
    }
  } else {
    return {}
  }
}

function pointerUp(annotation, e) {
  if (annotation.selection) {
    const { selection, geometry } = annotation
    if (!geometry) {
      return {}
    }
    switch (annotation.selection.mode) {
      case 'SELECTING':
        return {
          ...annotation,
          selection: {
            ...annotation.selection,
            showEditor: true,
            mode: 'EDITING'
          }
        }
      default:
        break
    }
  }
  return annotation
}

function pointerMove(annotation, e) {
  if (annotation.selection && annotation.selection.mode === 'SELECTING') {
    const { anchorX, anchorY } = annotation.selection
    const { x: newX, y: newY } = getCoordPercentage(e)
    const width = newX - anchorX
    const height = newY - anchorY

    return {
      ...annotation,
      geometry: {
        ...annotation.geometry,
        type: TYPE,
        x: width > 0 ? anchorX : newX,
        y: height > 0 ? anchorY : newY,
        width: Math.abs(width),
        height: Math.abs(height)
      }
    }
  }
  return annotation
}

export default {
  TYPE,
  intersects,
  area,
  methods
}


================================================
FILE: src/index.js
================================================
import Annotation from './components/Annotation'
export { default as defaultProps } from './components/defaultProps'

export default Annotation


================================================
FILE: src/selectors.js
================================================
export { default as RectangleSelector } from './hocs/RectangleSelector'
export { default as PointSelector } from './hocs/PointSelector'
export { default as OvalSelector } from './hocs/OvalSelector'


================================================
FILE: src/types/index.d.ts
================================================
declare module "react-image-annotation" {
  export interface IGeometry {
    type: string;
    x?: number;
    y?: number;
    height?: number;
    width?: number;
  }
  export interface ISelector {
    TYPE: string;
    intersects: (
      { x, y }: { x: number; y: number },
      geometry: IGeometry,
      container: { width: number; height: number }
    ) => boolean;
    area: (
      geometry: IGeometry,
      container: { width: number; height: number }
    ) => number;
    methods: {
      onMouseUp?: (annotation: IAnnotation, e: any) => IAnnotation | {};
      onMouseDown?: (annotation: IAnnotation, e: any) => IAnnotation | {};
      onMouseMove?: (annotation: IAnnotation, e: any) => IAnnotation | {};
      onClick?: (annotation: IAnnotation, e: any) => IAnnotation | {};
    };
  }
  export interface IAnnotation {
    selection?: {
      mode: string;
      showEditor: boolean;
    };
    geometry: IGeometry;
    data: {
      text: string;
      id?: number;
    };
  }
  interface IAnnotationProps {
    src: string;
    alt?: string;
    innerRef?: (e: any) => any;
    onMouseUp?: (e: React.MouseEvent) => any;
    onMouseDown?: (e: React.MouseEvent) => any;
    onMouseMove?: (e: React.MouseEvent) => any;
    onClick?: (e: React.MouseEvent) => any;

    annotations: IAnnotation[];
    type?: string;
    selectors?: ISelector[];

    value: IAnnotation | {};
    onChange?: (e: any) => any;
    onSubmit?: (e: any) => any;

    activeAnnotationComparator?: (annotation: IAnnotation) => boolean;
    activeAnnotations?: IAnnotation[];

    disableAnnotation?: boolean;
    disableSelector?: boolean;
    renderSelector?: (
      { annotation, active }: { annotation: IAnnotation; active: boolean }
    ) => any;
    disableEditor?: boolean;
    renderEditor?: (
      {
        annotation,
        onChange,
        onSubmit
      }: {
        annotation: IAnnotation;
        onChange: (annotation: IAnnotation | {}) => any;
        onSubmit: (e?: any) => any;
      }
    ) => any;

    renderHighlight?: (
      { annotation, active }: { annotation: IAnnotation; active: boolean }
    ) => any;
    renderContent?: ({ annotation }: { annotation: IAnnotation }) => any;

    disableOverlay?: boolean;
    renderOverlay?: () => any;
    allowTouch: boolean;
  }

  class Annotation extends React.Component<IAnnotationProps, {}> {}
  export default Annotation;
}


================================================
FILE: src/utils/compose.js
================================================
export default function compose (...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}


================================================
FILE: src/utils/isMouseHovering.js
================================================
import React, { PureComponent as Component } from 'react'

const isMouseOverElement = ({ elem, e }) => {
  const { pageY, pageX } = e
  const { left, right, bottom, top } = elem.getBoundingClientRect()

  return pageX > left && pageX < right && pageY > top && pageY < bottom
}

const isMouseHovering = (key = 'isMouseHovering') => DecoratedComponent => {
  class IsMouseHovering extends Component {
    constructor() {
      super()

      this.state = {
        isHoveringOver: false
      }
    }

    componentDidMount() {
      document.addEventListener('mousemove', this.onMouseMove)
    }

    componentWillUnmount() {
      document.removeEventListener('mousemove', this.onMouseMove)
    }

    onMouseMove = e => {
      const elem = this.el

      this.setState({
        isHoveringOver: isMouseOverElement({ elem, e })
      })
    }

    render() {
      const hocProps = {
        [key]: {
          innerRef: el => this.el = el,
          isHoveringOver: this.state.isHoveringOver
        }
      }

      return (
        <DecoratedComponent
          {...this.props}
          {...hocProps}
        />
      )
    }
  }

  IsMouseHovering.displayName = `IsMouseHovering(${DecoratedComponent.displayName})`

  return IsMouseHovering
}

export default isMouseHovering


================================================
FILE: src/utils/offsetCoordinates.js
================================================
const getMouseRelativeCoordinates = e => {
    // nativeEvent.offsetX gives inconsistent results when dragging
    // up and to the left rather than the more natural down and to the
    // right. The reason could be browser implementation (it is still experimental)
    // or it could be that nativeEvent offsets are based on target rather than
    // currentTarget.
    // To keep consistent behavior of the selector use the bounding client rect.
    const rect = e.currentTarget.getBoundingClientRect();
    const offsetX = e.clientX - rect.x;
    const offsetY = e.clientY - rect.y;

    return {
        x: offsetX / rect.width * 100,
        y: offsetY / rect.height * 100
    };
}

const clamp = (a, b, i) => Math.max(a, Math.min(b, i))
const getTouchRelativeCoordinates = e => {
  const touch = e.targetTouches[0]

  const boundingRect = e.currentTarget.getBoundingClientRect()
  // https://idiallo.com/javascript/element-postion
  // https://stackoverflow.com/questions/25630035/javascript-getboundingclientrect-changes-while-scrolling
  const offsetX = touch.pageX - boundingRect.left
  const offsetY = touch.pageY - (boundingRect.top + window.scrollY)

  return {
    x: clamp(0, 100, (offsetX / boundingRect.width) * 100),
    y: clamp(0, 100, (offsetY / boundingRect.height) * 100)
  }
}

const getCoordPercentage = (e) => {
  if (isTouchEvent(e)) {
    if (isValidTouchEvent(e)) {
      isTouchMoveEvent(e) && e.preventDefault()
      return getTouchRelativeCoordinates(e)
    } else {
      return {
        x: null
      }
    }
  } else {
    return getMouseRelativeCoordinates(e)
  }
}

const isTouchEvent = e => e.targetTouches !== undefined
const isValidTouchEvent = e => e.targetTouches.length === 1
const isTouchMoveEvent = e => e.type === 'touchmove'

export { getMouseRelativeCoordinates as getOffsetCoordPercentage, getCoordPercentage };


================================================
FILE: src/utils/withRelativeMousePos.js
================================================
import React, { PureComponent as Component } from 'react'
import { getOffsetCoordPercentage } from './offsetCoordinates';

const withRelativeMousePos = (key = 'relativeMousePos') => DecoratedComponent => {
  class WithRelativeMousePos extends Component {
    state = { x: null, y: null }

    innerRef = el => {
      this.container = el
    }

    onMouseMove = (e) => {
      const xystate = getOffsetCoordPercentage(e, this.container);
      this.setState(xystate);
    }
    onTouchMove = (e) => {
      if (e.targetTouches.length === 1) {
        const touch = e.targetTouches[0]

        const offsetX = touch.pageX - this.container.offsetParent.offsetLeft
        const offsetY = touch.pageY - this.container.offsetParent.offsetTop

        this.setState({
          x: (offsetX / this.container.width) * 100,
          y: (offsetY / this.container.height) * 100
        })
      }
    }

    onMouseLeave = (e) => {
      this.setState({ x: null, y: null })
    }
    onTouchLeave = (e) => {
      this.setState({ x: null, y: null })
    }

    render () {
      const hocProps = {
        [key]: {
          innerRef: this.innerRef,
          onMouseMove: this.onMouseMove,
          onMouseLeave: this.onMouseLeave,
          onTouchMove: this.onTouchMove,
          onTouchLeave: this.onTouchLeave,
          x: this.state.x,
          y: this.state.y
        }
      }

      return (
        <DecoratedComponent
          {...this.props}
          {...hocProps}
        />
      )
    }
  }

  WithRelativeMousePos.displayName = `withRelativeMousePos(${DecoratedComponent.displayName})`

  return WithRelativeMousePos
}

export default withRelativeMousePos


================================================
FILE: tests/.eslintrc
================================================
{
  "env": {
    "mocha": true
  }
}


================================================
FILE: tests/Annotation.spec.js
================================================
import { mount } from 'enzyme'
import { expect } from 'chai'
import React from 'react'

import Annotation from '../src/components/Annotation'

const requiredProps = {
  annotations: []
}

describe('Annotation', () => {
  describe('render', () => {
    it('renders <Annotation />', () => {
      const wrapper = mount(<Annotation {...requiredProps} />)
      expect(wrapper.find('Annotation')).to.have.length(1)
    })
  })
})


================================================
FILE: tests/index.test.js
================================================
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

Enzyme.configure({ adapter: new Adapter() })

let context = require.context('./', true, /\.spec\.js$/)
context.keys().forEach(context)


================================================
FILE: tests/selectors/OvalSelector.spec.js
================================================
import { mount } from 'enzyme'
import { expect } from 'chai'
import React from 'react'

import { OvalSelector as selector } from '../../src/selectors'

function createOval ({ x, y, width, height } = { x: 10, y: 10, width: 20, height: 10 }) {
  return {
    x, y, width, height
  }
}

describe('OvalSelector', () => {
  describe('TYPE', () => {
    it('should be a defined string', () => {
      expect(selector.TYPE).to.be.a('string')
    })
  })

  describe('intersects', () => {
    it('should return true when point is inside geometry', () => {
      expect(
        selector.intersects({ x: 15, y: 15 }, createOval())
      ).to.be.true

      const x = 15
      const y = 17
      expect(
        selector.intersects({ x, y }, createOval())
      ).to.be.true
    })
    it('should return false when point is outside of geometry', () => {
      expect(selector.intersects({ x: 0, y: 0 }, createOval())).to.be.false
      expect(selector.intersects({ x: 10, y: 0 }, createOval())).to.be.false
      expect(selector.intersects({ x: 0, y: 10 }, createOval())).to.be.false
      expect(selector.intersects({ x: 30, y: 30 }, createOval())).to.be.false
    })
  })

  describe('area', () => {
    it('should return geometry area', () => {
      expect(selector.area(createOval())).to.equal(157.07963267948966)
    })
  })

  describe('methods', () => {
    xit('should be defined')
  })
})


================================================
FILE: tests/selectors/PointSelector.spec.js
================================================
import { mount } from 'enzyme'
import { expect } from 'chai'
import React from 'react'

import { PointSelector as selector } from '../../src/selectors'

function createPoint ({ x, y } = { x: 10, y: 10 }) {
  return { x, y }
}

function createContainer({ width, height } = { width: 100, height: 100 }) {
  return { width, height }
}

describe('PoinntSelector', () => {
  describe('TYPE', () => {
    it('should be a defined string', () => {
      expect(selector.TYPE).to.be.a('string')
    })
  })

  describe('intersects', () => {
    it('should return true when point is inside geometry', () => {
      expect(
        selector.intersects({ x: 10, y: 10 }, createPoint(), createContainer())
      ).to.be.true
    })
    it('should return false when point is outside of geometry', () => {
      expect(selector.intersects({ x: 0, y: 0 }, createPoint(), createContainer())).to.be.false
      expect(selector.intersects({ x: 10, y: 0 }, createPoint(), createContainer())).to.be.false
      expect(selector.intersects({ x: 0, y: 10 }, createPoint(), createContainer())).to.be.false
      expect(selector.intersects({ x: 30, y: 30 }, createPoint(), createContainer())).to.be.false
    })
  })

  describe('area', () => {
    it('should return geometry area', () => {
      expect(
        selector.area(createPoint(), createContainer())
      ).to.equal(36)
    })
    it('should return geometry area based on container', () => {
      expect(
        selector.area(createPoint(), createContainer({ width: 200, height: 200 }))
      ).to.equal(9)
    })
  })

  describe('methods', () => {
    xit('should be defined')
  })
})


================================================
FILE: tests/selectors/RectangleSelector.spec.js
================================================
import { mount } from 'enzyme'
import { expect } from 'chai'
import React from 'react'

import { RectangleSelector as selector } from '../../src/selectors'

function createRect ({ x, y, width, height } = { x: 10, y: 10, width: 10, height: 10 }) {
  return {
    x, y, width, height
  }
}

describe('RectangleSelector', () => {
  describe('TYPE', () => {
    it('should be a defined string', () => {
      expect(selector.TYPE).to.be.a('string')
    })
  })

  describe('intersects', () => {
    it('should return true when point is on top left of geometry', () => {
      expect(
        selector.intersects({ x: 10, y: 10 }, createRect())
      ).to.be.true
    })
    it('should return true when point is on top right of geometry', () => {
      expect(
        selector.intersects({ x: 20, y: 10 }, createRect())
      ).to.be.true
    })
    it('should return true when point is on bottom left of geometry', () => {
      expect(
        selector.intersects({ x: 10, y: 20 }, createRect())
      ).to.be.true
    })
    it('should return true when point is on bottom right of geometry', () => {
      expect(
        selector.intersects({ x: 20, y: 20 }, createRect())
      ).to.be.true
    })
    it('should return true when point is inside geometry', () => {
      expect(
        selector.intersects({ x: 15, y: 15 }, createRect())
      ).to.be.true
    })
    it('should return false when point is outside of geometry', () => {
      expect(selector.intersects({ x: 0, y: 0 }, createRect())).to.be.false
      expect(selector.intersects({ x: 10, y: 0 }, createRect())).to.be.false
      expect(selector.intersects({ x: 0, y: 10 }, createRect())).to.be.false
      expect(selector.intersects({ x: 30, y: 30 }, createRect())).to.be.false
    })
  })

  describe('area', () => {
    it('should return geometry area', () => {
      expect(selector.area(createRect({ width: 10, height: 10 }))).to.equal(100)
    })
  })

  describe('methods', () => {
    xit('should be defined')
  })
})
Download .txt
gitextract_r5h__py7/

├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── demo/
│   ├── public/
│   │   └── 404.html
│   └── src/
│       ├── App.js
│       ├── components/
│       │   ├── Button/
│       │   │   └── index.js
│       │   ├── Docs/
│       │   │   └── index.js
│       │   ├── Footer/
│       │   │   └── index.js
│       │   ├── GithubStarLink/
│       │   │   └── index.js
│       │   ├── Highlight/
│       │   │   └── index.js
│       │   ├── Home/
│       │   │   ├── index.js
│       │   │   └── simple.txt
│       │   ├── NavBar/
│       │   │   └── index.js
│       │   ├── Root/
│       │   │   └── index.js
│       │   └── Samples/
│       │       ├── Custom/
│       │       │   └── index.js
│       │       ├── Linked/
│       │       │   ├── index.js
│       │       │   └── index.txt
│       │       ├── Multiple/
│       │       │   ├── index.js
│       │       │   └── index.txt
│       │       ├── Simple/
│       │       │   └── index.js
│       │       ├── Threaded/
│       │       │   └── index.js
│       │       └── Touch/
│       │           ├── index.js
│       │           └── index.txt
│       ├── index.css
│       ├── index.html
│       ├── index.js
│       ├── mocks.js
│       └── registerServiceWorker.js
├── nwb.config.js
├── package.json
├── public/
│   └── 404.html
├── src/
│   ├── components/
│   │   ├── Annotation.js
│   │   ├── Content/
│   │   │   └── index.js
│   │   ├── Editor/
│   │   │   └── index.js
│   │   ├── FancyRectangle/
│   │   │   └── index.js
│   │   ├── Oval/
│   │   │   └── index.js
│   │   ├── Overlay/
│   │   │   └── index.js
│   │   ├── Point/
│   │   │   └── index.js
│   │   ├── Rectangle/
│   │   │   └── index.js
│   │   ├── TextEditor/
│   │   │   └── index.js
│   │   └── defaultProps.js
│   ├── hocs/
│   │   ├── OvalSelector.js
│   │   ├── PointSelector.js
│   │   └── RectangleSelector.js
│   ├── index.js
│   ├── selectors.js
│   ├── types/
│   │   └── index.d.ts
│   └── utils/
│       ├── compose.js
│       ├── isMouseHovering.js
│       ├── offsetCoordinates.js
│       └── withRelativeMousePos.js
└── tests/
    ├── .eslintrc
    ├── Annotation.spec.js
    ├── index.test.js
    └── selectors/
        ├── OvalSelector.spec.js
        ├── PointSelector.spec.js
        └── RectangleSelector.spec.js
Download .txt
SYMBOL INDEX (86 symbols across 27 files)

FILE: demo/src/components/Docs/index.js
  class Docs (line 28) | class Docs extends Component {
    method render (line 29) | render () {

FILE: demo/src/components/Home/index.js
  class App (line 34) | class App extends Component {
    method render (line 35) | render () {

FILE: demo/src/components/Samples/Custom/index.js
  function renderSelector (line 29) | function renderSelector ({ annotation, active }) {
  function renderHighlight (line 46) | function renderHighlight ({ annotation, active }) {
  function renderContent (line 65) | function renderContent ({ annotation }) {
  function renderEditor (line 86) | function renderEditor (props) {
  function renderOverlay (line 115) | function renderOverlay () {
  class Custom (line 133) | class Custom extends Component {
    method render (line 165) | render () {

FILE: demo/src/components/Samples/Linked/index.js
  class Linked (line 25) | class Linked extends Component {
    method render (line 84) | render () {

FILE: demo/src/components/Samples/Multiple/index.js
  class Multiple (line 14) | class Multiple extends Component {
    method render (line 47) | render () {

FILE: demo/src/components/Samples/Simple/index.js
  class Simple (line 7) | class Simple extends Component {
    method render (line 32) | render () {

FILE: demo/src/components/Samples/Threaded/index.js
  class ThreadedEditor (line 93) | class ThreadedEditor extends Component {
    method render (line 121) | render () {
  class ThreadedContent (line 150) | class ThreadedContent extends Component {
    method renderComment (line 159) | renderComment (comment) {
    method render (line 170) | render () {
  class Threaded (line 222) | class Threaded extends Component {
    method render (line 294) | render () {

FILE: demo/src/components/Samples/Touch/index.js
  class Multiple (line 14) | class Multiple extends Component {
    method render (line 52) | render() {

FILE: demo/src/registerServiceWorker.js
  function register (line 21) | function register () {
  function registerValidSW (line 46) | function registerValidSW (swUrl) {
  function checkValidServiceWorker (line 75) | function checkValidServiceWorker (swUrl) {
  function unregister (line 102) | function unregister () {

FILE: src/components/Annotation.js
  method componentDidMount (line 93) | componentDidMount() {
  method componentDidUpdate (line 118) | componentDidUpdate(prevProps) {
  method render (line 232) | render () {

FILE: src/components/Content/index.js
  function Content (line 16) | function Content (props) {

FILE: src/components/Editor/index.js
  function Editor (line 31) | function Editor (props) {

FILE: src/components/FancyRectangle/index.js
  function FancyRectangle (line 17) | function FancyRectangle (props) {

FILE: src/components/Oval/index.js
  function Oval (line 12) | function Oval (props) {

FILE: src/components/Point/index.js
  function Point (line 18) | function Point (props) {

FILE: src/components/Rectangle/index.js
  function Rectangle (line 11) | function Rectangle (props) {

FILE: src/components/TextEditor/index.js
  function TextEditor (line 37) | function TextEditor (props) {

FILE: src/hocs/OvalSelector.js
  constant TYPE (line 5) | const TYPE = 'OVAL'
  function intersects (line 7) | function intersects({ x, y }, geometry) {
  function area (line 18) | function area(geometry) {
  method onTouchStart (line 26) | onTouchStart(annotation, e) {
  method onTouchEnd (line 29) | onTouchEnd(annotation, e) {
  method onTouchMove (line 32) | onTouchMove(annotation, e) {
  method onMouseDown (line 35) | onMouseDown(annotation, e) {
  method onMouseUp (line 38) | onMouseUp(annotation, e) {
  method onMouseMove (line 41) | onMouseMove(annotation, e) {
  function pointerDown (line 46) | function pointerDown(annotation, e) {
  function pointerUp (line 65) | function pointerUp(annotation, e) {
  function pointerMove (line 90) | function pointerMove(annotation, e) {

FILE: src/hocs/PointSelector.js
  constant MARGIN (line 2) | const MARGIN = 6
  constant TYPE (line 9) | const TYPE = 'POINT'
  function intersects (line 11) | function intersects ({ x, y }, geometry, container) {
  function area (line 22) | function area (geometry, container) {
  method onClick (line 29) | onClick (annotation, e) {

FILE: src/hocs/RectangleSelector.js
  constant TYPE (line 3) | const TYPE = 'RECTANGLE'
  function intersects (line 5) | function intersects({ x, y }, geometry) {
  function area (line 14) | function area(geometry) {
  method onTouchStart (line 19) | onTouchStart(annotation, e) {
  method onTouchEnd (line 22) | onTouchEnd(annotation, e) {
  method onTouchMove (line 25) | onTouchMove(annotation, e) {
  method onMouseDown (line 28) | onMouseDown(annotation, e) {
  method onMouseUp (line 31) | onMouseUp(annotation, e) {
  method onMouseMove (line 34) | onMouseMove(annotation, e) {
  function pointerDown (line 39) | function pointerDown(annotation, e) {
  function pointerUp (line 56) | function pointerUp(annotation, e) {
  function pointerMove (line 79) | function pointerMove(annotation, e) {

FILE: src/types/index.d.ts
  type IGeometry (line 2) | interface IGeometry {
  type ISelector (line 9) | interface ISelector {
  type IAnnotation (line 27) | interface IAnnotation {
  type IAnnotationProps (line 38) | interface IAnnotationProps {
  class Annotation (line 86) | class Annotation extends React.Component<IAnnotationProps, {}> {}

FILE: src/utils/compose.js
  function compose (line 1) | function compose (...funcs) {

FILE: src/utils/isMouseHovering.js
  class IsMouseHovering (line 11) | class IsMouseHovering extends Component {
    method constructor (line 12) | constructor() {
    method componentDidMount (line 20) | componentDidMount() {
    method componentWillUnmount (line 24) | componentWillUnmount() {
    method render (line 36) | render() {

FILE: src/utils/withRelativeMousePos.js
  class WithRelativeMousePos (line 5) | class WithRelativeMousePos extends Component {
    method render (line 37) | render () {

FILE: tests/selectors/OvalSelector.spec.js
  function createOval (line 7) | function createOval ({ x, y, width, height } = { x: 10, y: 10, width: 20...

FILE: tests/selectors/PointSelector.spec.js
  function createPoint (line 7) | function createPoint ({ x, y } = { x: 10, y: 10 }) {
  function createContainer (line 11) | function createContainer({ width, height } = { width: 100, height: 100 }) {

FILE: tests/selectors/RectangleSelector.spec.js
  function createRect (line 7) | function createRect ({ x, y, width, height } = { x: 10, y: 10, width: 10...
Condensed preview — 60 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (97K chars).
[
  {
    "path": ".gitignore",
    "chars": 72,
    "preview": "/coverage\n/demo/dist\n/es\n/lib\n/node_modules\n/umd\nnpm-debug.log*\n.vscode/"
  },
  {
    "path": ".travis.yml",
    "chars": 296,
    "preview": "sudo: false\n\nlanguage: node_js\nnode_js:\n  - 8\n\nbefore_install:\n  - npm install codecov.io coveralls\n\nafter_success:\n  - "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 380,
    "preview": "## 0.9.8\n\n### Improvements\n\n- Add Type Definitions for Typescript (#12) (thanks @danilofuchs)\n- Add support for `childre"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 745,
    "preview": "## Prerequisites\n\n[Node.js](http://nodejs.org/) >= v4 must be installed.\n\n## Installation\n\n- Running `npm install` in th"
  },
  {
    "path": "LICENSE.md",
    "chars": 1098,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2018-present, Arian Allenson Valdez.\n\nPermission is hereby granted, free of charge,"
  },
  {
    "path": "README.md",
    "chars": 6875,
    "preview": "React Image Annotation\n=========================\n\nAn infinitely customizable image annotation library built on React\n\n!["
  },
  {
    "path": "demo/public/404.html",
    "chars": 1851,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Single Page Apps for GitHub Pages</title>\n    <scr"
  },
  {
    "path": "demo/src/App.js",
    "chars": 769,
    "preview": "import React from 'react'\nimport { BrowserRouter as Router, Route } from 'react-router-dom'\nimport styled from 'styled-c"
  },
  {
    "path": "demo/src/components/Button/index.js",
    "chars": 668,
    "preview": "import styled, { css } from 'styled-components'\nimport { Link } from 'react-router-dom'\n\nconst styles = css`\n  backgroun"
  },
  {
    "path": "demo/src/components/Docs/index.js",
    "chars": 1892,
    "preview": "import React, { Component } from 'react'\nimport styled from 'styled-components'\nimport Highlight from '../Highlight'\nimp"
  },
  {
    "path": "demo/src/components/Footer/index.js",
    "chars": 457,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst Footer = styled.div`\n  color: #666;\n  padding: 1"
  },
  {
    "path": "demo/src/components/GithubStarLink/index.js",
    "chars": 283,
    "preview": "import React from 'react'\n\nexport default () => (\n  <a\n    className='github-button'\n    href='https://github.com/Secret"
  },
  {
    "path": "demo/src/components/Highlight/index.js",
    "chars": 287,
    "preview": "import React from 'react'\nimport SyntaxHighlighter from 'react-syntax-highlighter/prism-light'\nimport prism from 'react-"
  },
  {
    "path": "demo/src/components/Home/index.js",
    "chars": 1327,
    "preview": "import React, { Component } from 'react'\nimport styled from 'styled-components'\nimport Simple from '../Samples/Simple'\ni"
  },
  {
    "path": "demo/src/components/Home/simple.txt",
    "chars": 893,
    "preview": "import React, { Component } from 'react'\nimport Annotation from 'react-image-annotation'\n\nexport default class Simple ex"
  },
  {
    "path": "demo/src/components/NavBar/index.js",
    "chars": 1144,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\nimport { Link } from 'react-router-dom'\n\nconst Header ="
  },
  {
    "path": "demo/src/components/Root/index.js",
    "chars": 278,
    "preview": "import styled from 'styled-components'\n\nexport default styled.div`\n  font-family: 'Open Sans', sans-serif;\n  margin: 0 a"
  },
  {
    "path": "demo/src/components/Samples/Custom/index.js",
    "chars": 3819,
    "preview": "import React, { Component } from 'react'\nimport Annotation from '../../../../../src'\nimport {\n  PointSelector,\n  Rectang"
  },
  {
    "path": "demo/src/components/Samples/Linked/index.js",
    "chars": 2675,
    "preview": "import React, { Component } from 'react'\nimport styled from 'styled-components'\nimport Annotation from '../../../../../s"
  },
  {
    "path": "demo/src/components/Samples/Linked/index.txt",
    "chars": 1247,
    "preview": "classs React extends Component {\n  state = {\n    activeAnnotations: []\n  }\n\n  // ...other React code\n\n  onMouseOver = (i"
  },
  {
    "path": "demo/src/components/Samples/Multiple/index.js",
    "chars": 1803,
    "preview": "import React, { Component } from 'react'\nimport Annotation from '../../../../../src'\nimport {\n  PointSelector,\n  Rectang"
  },
  {
    "path": "demo/src/components/Samples/Multiple/index.txt",
    "chars": 1474,
    "preview": "import React, { Component } from 'react'\nimport Annotation from 'react-image-annotation'\nimport {\n  PointSelector,\n  Rec"
  },
  {
    "path": "demo/src/components/Samples/Simple/index.js",
    "chars": 997,
    "preview": "import React, { Component } from 'react'\nimport Annotation from '../../../../../src'\n\nimport Root from '../../Root'\nimpo"
  },
  {
    "path": "demo/src/components/Samples/Threaded/index.js",
    "chars": 7176,
    "preview": "import React, { Component } from 'react'\nimport Annotation from '../../../../../src'\n\nimport styled, { keyframes } from "
  },
  {
    "path": "demo/src/components/Samples/Touch/index.js",
    "chars": 2245,
    "preview": "import React, { Component } from 'react'\nimport Annotation from '../../../../../src'\nimport {\n  PointSelector,\n  Rectang"
  },
  {
    "path": "demo/src/components/Samples/Touch/index.txt",
    "chars": 830,
    "preview": "class Touch extends Component {\n  state = { allowTouch: true }\n\n  toggleAllowTouch = () => {\n    this.setState((prevStat"
  },
  {
    "path": "demo/src/index.css",
    "chars": 110,
    "preview": "@import url('https://fonts.googleapis.com/css?family=Montserrat:700|Open+Sans');\n\nhtml, body {\n  margin: 0;\n}\n"
  },
  {
    "path": "demo/src/index.html",
    "chars": 1836,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initia"
  },
  {
    "path": "demo/src/index.js",
    "chars": 413,
    "preview": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport './index.css'\nimport App from './App'\nimport registerS"
  },
  {
    "path": "demo/src/mocks.js",
    "chars": 560,
    "preview": "import {\n  RectangleSelector,\n  OvalSelector\n} from '../../src/selectors'\n\nexport default {\n  annotations: [\n    {\n     "
  },
  {
    "path": "demo/src/registerServiceWorker.js",
    "chars": 4003,
    "preview": "// In production, we register a service worker to serve assets from local cache.\n\n// This lets the app load faster on su"
  },
  {
    "path": "nwb.config.js",
    "chars": 465,
    "preview": "const path = require('path')\n\nmodule.exports = {\n  type: 'react-component',\n  npm: {\n    esModules: true,\n    umd: {\n   "
  },
  {
    "path": "package.json",
    "chars": 1513,
    "preview": "{\n  \"name\": \"react-image-annotation\",\n  \"version\": \"0.9.10\",\n  \"description\": \"react-image-annotation React component\",\n"
  },
  {
    "path": "public/404.html",
    "chars": 1851,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Single Page Apps for GitHub Pages</title>\n    <scr"
  },
  {
    "path": "src/components/Annotation.js",
    "chars": 8501,
    "preview": "import React, { Component } from 'react'\nimport T from 'prop-types'\nimport styled from 'styled-components'\nimport compos"
  },
  {
    "path": "src/components/Content/index.js",
    "chars": 850,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst Container = styled.div`\n  background: white;\n  b"
  },
  {
    "path": "src/components/Editor/index.js",
    "chars": 1355,
    "preview": "import React from 'react'\nimport styled, { keyframes } from 'styled-components'\nimport TextEditor from '../TextEditor'\n\n"
  },
  {
    "path": "src/components/FancyRectangle/index.js",
    "chars": 1246,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst Box = styled.div`\n  background: rgba(0, 0, 0, 0."
  },
  {
    "path": "src/components/Oval/index.js",
    "chars": 784,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst Container = styled.div`\n  border: dashed 2px bla"
  },
  {
    "path": "src/components/Overlay/index.js",
    "chars": 374,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nexport default styled.div`\n  background: rgba(0, 0, 0,"
  },
  {
    "path": "src/components/Point/index.js",
    "chars": 619,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst Container = styled.div`\n  border: solid 3px whit"
  },
  {
    "path": "src/components/Rectangle/index.js",
    "chars": 776,
    "preview": "import React from 'react'\nimport styled from 'styled-components'\n\nconst Container = styled.div`\n  border: dashed 2px bla"
  },
  {
    "path": "src/components/TextEditor/index.js",
    "chars": 1111,
    "preview": "import React from 'react'\nimport styled, { keyframes } from 'styled-components'\n\nconst Inner = styled.div`\n  padding: 8p"
  },
  {
    "path": "src/components/defaultProps.js",
    "chars": 2473,
    "preview": "import React from 'react'\n\nimport Point from './Point'\nimport Editor from './Editor'\nimport FancyRectangle from './Fancy"
  },
  {
    "path": "src/hocs/OvalSelector.js",
    "chars": 2439,
    "preview": "import { getCoordPercentage } from '../utils/offsetCoordinates';\n\nconst square = n => Math.pow(n, 2)\n\nexport const TYPE "
  },
  {
    "path": "src/hocs/PointSelector.js",
    "chars": 1220,
    "preview": "import { getCoordPercentage } from '../utils/offsetCoordinates';\nconst MARGIN = 6\n\nconst marginToPercentage = (container"
  },
  {
    "path": "src/hocs/RectangleSelector.js",
    "chars": 2309,
    "preview": "import { getCoordPercentage } from '../utils/offsetCoordinates';\n\nexport const TYPE = 'RECTANGLE'\n\nexport function inter"
  },
  {
    "path": "src/index.js",
    "chars": 144,
    "preview": "import Annotation from './components/Annotation'\nexport { default as defaultProps } from './components/defaultProps'\n\nex"
  },
  {
    "path": "src/selectors.js",
    "chars": 198,
    "preview": "export { default as RectangleSelector } from './hocs/RectangleSelector'\nexport { default as PointSelector } from './hocs"
  },
  {
    "path": "src/types/index.d.ts",
    "chars": 2390,
    "preview": "declare module \"react-image-annotation\" {\n  export interface IGeometry {\n    type: string;\n    x?: number;\n    y?: numbe"
  },
  {
    "path": "src/utils/compose.js",
    "chars": 215,
    "preview": "export default function compose (...funcs) {\n  if (funcs.length === 0) {\n    return arg => arg\n  }\n\n  if (funcs.length ="
  },
  {
    "path": "src/utils/isMouseHovering.js",
    "chars": 1281,
    "preview": "import React, { PureComponent as Component } from 'react'\n\nconst isMouseOverElement = ({ elem, e }) => {\n  const { pageY"
  },
  {
    "path": "src/utils/offsetCoordinates.js",
    "chars": 1862,
    "preview": "const getMouseRelativeCoordinates = e => {\n    // nativeEvent.offsetX gives inconsistent results when dragging\n    // up"
  },
  {
    "path": "src/utils/withRelativeMousePos.js",
    "chars": 1670,
    "preview": "import React, { PureComponent as Component } from 'react'\nimport { getOffsetCoordPercentage } from './offsetCoordinates'"
  },
  {
    "path": "tests/.eslintrc",
    "chars": 37,
    "preview": "{\n  \"env\": {\n    \"mocha\": true\n  }\n}\n"
  },
  {
    "path": "tests/Annotation.spec.js",
    "chars": 426,
    "preview": "import { mount } from 'enzyme'\nimport { expect } from 'chai'\nimport React from 'react'\n\nimport Annotation from '../src/c"
  },
  {
    "path": "tests/index.test.js",
    "chars": 210,
    "preview": "import Enzyme from 'enzyme'\nimport Adapter from 'enzyme-adapter-react-16'\n\nEnzyme.configure({ adapter: new Adapter() })\n"
  },
  {
    "path": "tests/selectors/OvalSelector.spec.js",
    "chars": 1389,
    "preview": "import { mount } from 'enzyme'\nimport { expect } from 'chai'\nimport React from 'react'\n\nimport { OvalSelector as selecto"
  },
  {
    "path": "tests/selectors/PointSelector.spec.js",
    "chars": 1625,
    "preview": "import { mount } from 'enzyme'\nimport { expect } from 'chai'\nimport React from 'react'\n\nimport { PointSelector as select"
  },
  {
    "path": "tests/selectors/RectangleSelector.spec.js",
    "chars": 1993,
    "preview": "import { mount } from 'enzyme'\nimport { expect } from 'chai'\nimport React from 'react'\n\nimport { RectangleSelector as se"
  }
]

About this extraction

This page contains the full source code of the Secretmapper/react-image-annotation GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 60 files (87.7 KB), approximately 23.8k tokens, and a symbol index with 86 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!