├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE.txt ├── README.md ├── ROADMAP.md ├── bower.json ├── dist ├── react-image-mapper.js └── react-image-mapper.min.js ├── example └── src │ ├── .gitignore │ ├── example.js │ ├── example.less │ └── index.html ├── gulpfile.js ├── lib └── ImageMapper.js ├── package-lock.json ├── package.json ├── src └── ImageMapper.js └── test └── ImageMapper.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | indent_style = tab 11 | 12 | [*.json] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .publish/* 2 | dist/* 3 | example/dist/* 4 | lib/* 5 | node_modules/* 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "rules": { 11 | "curly": [2, "multi-or-nest"], 12 | "quotes": [2, "single", "avoid-escape"], 13 | "react/display-name": 0, 14 | "react/jsx-boolean-value": 1, 15 | "jsx-quotes": 1, 16 | "react/jsx-no-undef": 1, 17 | "react/jsx-sort-props": 0, 18 | "react/jsx-sort-prop-types": 1, 19 | "react/jsx-uses-react": 1, 20 | "react/jsx-uses-vars": 1, 21 | "react/no-did-mount-set-state": 1, 22 | "react/no-did-update-set-state": 1, 23 | "react/no-multi-comp": 1, 24 | "react/no-unknown-property": 1, 25 | "react/prop-types": 1, 26 | "react/react-in-jsx-scope": 1, 27 | "react/self-closing-comp": 1, 28 | "react/wrap-multilines": 1, 29 | "semi": 2, 30 | "strict": 0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage tools 11 | lib-cov 12 | coverage 13 | coverage.html 14 | .cover* 15 | 16 | # Dependency directory 17 | node_modules 18 | 19 | # Example build directory 20 | example/dist 21 | .publish 22 | 23 | # Editor and other tmp files 24 | *.swp 25 | *.un~ 26 | *.iml 27 | *.ipr 28 | *.iws 29 | *.sublime-* 30 | .idea/ 31 | *.DS_Store 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Coldiary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOT MAINTAINED 2 | 3 | ## This repository is not maintened anymore (and since a long time). 4 | 5 | Sorry about all unanswered issues, I don't have time to maintain this package. 6 | If I had chance, I would rewrite this from scratch (maybe using a more typed and native approach). 7 | 8 | Feel free to fork and enhance that libray if you need. 9 | 10 | # react-image-mapper 11 | 12 | React Component to highlight interactive zones in images 13 | 14 | 15 | ## Demo & Examples 16 | 17 | Live demo: [coldiary.github.io/react-image-mapper](http://coldiary.github.io/react-image-mapper/) 18 | 19 | To build the example locally, run: 20 | 21 | ``` 22 | npm install 23 | npm start 24 | ``` 25 | 26 | Then open [`localhost:8000`](http://localhost:8000) in a browser. 27 | 28 | ## Installation 29 | 30 | The easiest way to use react-image-mapper is to install it from NPM and include it in your own React build process (using [Browserify](http://browserify.org), [Webpack](http://webpack.github.io/), etc). 31 | 32 | You can also use the standalone build by including `dist/react-image-mapper.js` in your page. If you use this, make sure you have already included React, and it is available as a global variable. 33 | 34 | ``` 35 | npm install react-image-mapper --save 36 | ``` 37 | 38 | 39 | ## Usage 40 | 41 | Import the component as you normally do, and add it wherever you like in your JSX views as below: 42 | 43 | ```javascript 44 | // ES5 require 45 | var ImageMapper = require('react-image-mapper'); 46 | 47 | // ES6 import 48 | import ImageMapper from 'react-image-mapper'; 49 | 50 | 51 | ``` 52 | 53 | ### Properties 54 | 55 | |Props|type|Description|default| 56 | |---|---|---|---| 57 | |**src**|*string*|Image source url| **required**| 58 | |**map**|*string*|Mapping description| `{ name: generated, areas: [ ] }`
(see below) | 59 | |**fillColor**|*string*|Fill color of the highlighted zone|`rgba(255, 255, 255, 0.5)`| 60 | |**strokeColor**|*string*|Border color of the highlighted zone|`rgba(0, 0, 0, 0.5)`| 61 | |**lineWidth**|*number*|Border thickness of the highlighted zone|`1`| 62 | |**width**|*number*|Image width|`Displayed width`| 63 | |**height**|*number*|Image height|`Displayed height`| 64 | |**active**|*bool*|Enable/Disable highlighting|`true`| 65 | |**imgWidth**|*number*|Original image width|`null`| 66 | 67 | |Props callbacks|Called on|signature| 68 | |---|---|---| 69 | |**onLoad**|Image loading and canvas initialization completed|`(): void`| 70 | |**onMouseEnter**|Hovering a zone in image|`(area: obj, index: num, event): void`| 71 | |**onMouseLeave**|Leaving a zone in image|`(area: obj, index: num, event): void`| 72 | |**onMouseMove**|Moving mouse on a zone in image|`(area: obj, index: num, event): void`| 73 | |**onClick**|Click on a zone in image|`(area: obj, index: num, event): void`| 74 | |**onImageClick**|Click outside of a zone in image|`(event): void`| 75 | |**onImageMouseMove**|Moving mouse on the image itself|`(event): void`| 76 | 77 | Map is an object describing highlighted areas in the image. 78 | 79 | Its structure is similar to the HTML syntax of mapping: 80 | 81 | - **map**: (*object*) Object to describe highlighted zones 82 | - **name**: (*string*) Name of the map, used to bind to the image. 83 | - **areas**: (*array*) Array of **area objects** 84 | - **area**: (*object*) Shaped like below : 85 | 86 | |Property| type|Description| 87 | |---|:---:|---| 88 | |**_id**|*string*|Uniquely identify an area. Index in array is used if this value is not provided.| 89 | |**shape**|*string*|Either `rect`, `circle` or `poly`| 90 | |**coords**|*array of number*|Coordinates delimiting the zone according to the specified shape: | 91 | |**href**|*string*|Target link for a click in the zone (note that if you provide a onClick prop, `href` will be prevented)| 92 | 93 | When received from an event handler, an area is extended with the following properties: 94 | 95 | |Property| type|Description| 96 | |---|:---:|---| 97 | |**scaledCoords**|*array of number*|Scaled coordinates (see [Dynamic Scaling](#dynamic-scaling) below)| 98 | |**center**|*array of number*|Coordinates positionning the center or centroid of the area: `[X, Y]`| 99 | 100 | ## Dynamic scaling 101 | When a parent component updates the **width** prop on ``, the area coordinates also have to be scaled. This can be accomplied by specifying both the new **width** and a constant **imgWidth**. **imgWidth** is the width of the original image. `` will calculate the new coordinates for each area. For example: 102 | ```javascript 103 | /* assume that image is actually 1500px wide */ 104 | 105 | // this will be a 1:1 scale, areas will be 3x bigger than they should be 106 | 107 | 108 | // this will be the same 1:1 scale, same problem with areas being too big 109 | 110 | 111 | // this will scale the areas to 1/3rd, they will now fit the 500px image on the screen 112 | 113 | ``` 114 | 115 | ## Development (`src`, `lib` and the build process) 116 | 117 | **NOTE:** The source code for the component is in `src`. A transpiled CommonJS version (generated with Babel) is available in `lib` for use with node.js, browserify and webpack. A UMD bundle is also built to `dist`, which can be included without the need for any build system. 118 | 119 | To build, watch and serve the examples (which will also watch the component source), run `npm start`. If you just want to watch changes to `src` and rebuild `lib`, run `npm run watch` (this is useful if you are working with `npm link`). 120 | 121 | 122 | ### Notes & Contributions 123 | 124 | See header. 125 | 126 | 127 | ## License 128 | 129 | Distributed with an MIT License. See LICENSE.txt for more details 130 | 131 | Copyright (c) 2017 Coldiary. 132 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # ROADMAP 2 | 3 | ## Planned 4 | - [ ] Rewriting the library for recent Node 5 | 6 | ## Suggestions 7 | - [ ] Add prop for a cyclic startup animation showing interactive areas 8 | - [ ] Add prop for a "show all" area 9 | - [ ] Add area prop that link a named area to other one to highlight as well 10 | - [ ] Add area prop to allow grouping them 11 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-image-mapper", 3 | "version": "0.0.0", 4 | "description": "imageMapper", 5 | "main": "dist/react-image-mapper.min.js", 6 | "homepage": "https://github.com/coldiary/react-image-mapper", 7 | "authors": [ 8 | "Coldiary" 9 | ], 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "react", 17 | "react-component" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | ".editorconfig", 22 | ".gitignore", 23 | "package.json", 24 | "src", 25 | "node_modules", 26 | "example", 27 | "test" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /dist/react-image-mapper.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ImageMapper = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1) { 230 | for (var i = 1; i < arguments.length; i++) { 231 | args[i - 1] = arguments[i]; 232 | } 233 | } 234 | queue.push(new Item(fun, args)); 235 | if (queue.length === 1 && !draining) { 236 | runTimeout(drainQueue); 237 | } 238 | }; 239 | 240 | // v8 likes predictible objects 241 | function Item(fun, array) { 242 | this.fun = fun; 243 | this.array = array; 244 | } 245 | Item.prototype.run = function () { 246 | this.fun.apply(null, this.array); 247 | }; 248 | process.title = 'browser'; 249 | process.browser = true; 250 | process.env = {}; 251 | process.argv = []; 252 | process.version = ''; // empty string to avoid regexp issues 253 | process.versions = {}; 254 | 255 | function noop() {} 256 | 257 | process.on = noop; 258 | process.addListener = noop; 259 | process.once = noop; 260 | process.off = noop; 261 | process.removeListener = noop; 262 | process.removeAllListeners = noop; 263 | process.emit = noop; 264 | process.prependListener = noop; 265 | process.prependOnceListener = noop; 266 | 267 | process.listeners = function (name) { return [] } 268 | 269 | process.binding = function (name) { 270 | throw new Error('process.binding is not supported'); 271 | }; 272 | 273 | process.cwd = function () { return '/' }; 274 | process.chdir = function (dir) { 275 | throw new Error('process.chdir is not supported'); 276 | }; 277 | process.umask = function() { return 0; }; 278 | 279 | },{}],3:[function(require,module,exports){ 280 | /** 281 | * Copyright (c) 2013-present, Facebook, Inc. 282 | * 283 | * This source code is licensed under the MIT license found in the 284 | * LICENSE file in the root directory of this source tree. 285 | */ 286 | 287 | 'use strict'; 288 | 289 | var printWarning = function() {}; 290 | 291 | if ("production" !== 'production') { 292 | var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); 293 | var loggedTypeFailures = {}; 294 | var has = Function.call.bind(Object.prototype.hasOwnProperty); 295 | 296 | printWarning = function(text) { 297 | var message = 'Warning: ' + text; 298 | if (typeof console !== 'undefined') { 299 | console.error(message); 300 | } 301 | try { 302 | // --- Welcome to debugging React --- 303 | // This error was thrown as a convenience so that you can use this stack 304 | // to find the callsite that caused this warning to fire. 305 | throw new Error(message); 306 | } catch (x) {} 307 | }; 308 | } 309 | 310 | /** 311 | * Assert that the values match with the type specs. 312 | * Error messages are memorized and will only be shown once. 313 | * 314 | * @param {object} typeSpecs Map of name to a ReactPropType 315 | * @param {object} values Runtime values that need to be type-checked 316 | * @param {string} location e.g. "prop", "context", "child context" 317 | * @param {string} componentName Name of the component for error messages. 318 | * @param {?Function} getStack Returns the component stack. 319 | * @private 320 | */ 321 | function checkPropTypes(typeSpecs, values, location, componentName, getStack) { 322 | if ("production" !== 'production') { 323 | for (var typeSpecName in typeSpecs) { 324 | if (has(typeSpecs, typeSpecName)) { 325 | var error; 326 | // Prop type validation may throw. In case they do, we don't want to 327 | // fail the render phase where it didn't fail before. So we log it. 328 | // After these have been cleaned up, we'll let them throw. 329 | try { 330 | // This is intentionally an invariant that gets caught. It's the same 331 | // behavior as without this statement except with a better message. 332 | if (typeof typeSpecs[typeSpecName] !== 'function') { 333 | var err = Error( 334 | (componentName || 'React class') + ': ' + location + ' type `' + typeSpecName + '` is invalid; ' + 335 | 'it must be a function, usually from the `prop-types` package, but received `' + typeof typeSpecs[typeSpecName] + '`.' 336 | ); 337 | err.name = 'Invariant Violation'; 338 | throw err; 339 | } 340 | error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret); 341 | } catch (ex) { 342 | error = ex; 343 | } 344 | if (error && !(error instanceof Error)) { 345 | printWarning( 346 | (componentName || 'React class') + ': type specification of ' + 347 | location + ' `' + typeSpecName + '` is invalid; the type checker ' + 348 | 'function must return `null` or an `Error` but returned a ' + typeof error + '. ' + 349 | 'You may have forgotten to pass an argument to the type checker ' + 350 | 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 351 | 'shape all require an argument).' 352 | ); 353 | } 354 | if (error instanceof Error && !(error.message in loggedTypeFailures)) { 355 | // Only monitor this failure once because there tends to be a lot of the 356 | // same error. 357 | loggedTypeFailures[error.message] = true; 358 | 359 | var stack = getStack ? getStack() : ''; 360 | 361 | printWarning( 362 | 'Failed ' + location + ' type: ' + error.message + (stack != null ? stack : '') 363 | ); 364 | } 365 | } 366 | } 367 | } 368 | } 369 | 370 | /** 371 | * Resets warning cache when testing. 372 | * 373 | * @private 374 | */ 375 | checkPropTypes.resetWarningCache = function() { 376 | if ("production" !== 'production') { 377 | loggedTypeFailures = {}; 378 | } 379 | } 380 | 381 | module.exports = checkPropTypes; 382 | 383 | },{"./lib/ReactPropTypesSecret":7}],4:[function(require,module,exports){ 384 | /** 385 | * Copyright (c) 2013-present, Facebook, Inc. 386 | * 387 | * This source code is licensed under the MIT license found in the 388 | * LICENSE file in the root directory of this source tree. 389 | */ 390 | 391 | 'use strict'; 392 | 393 | var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); 394 | 395 | function emptyFunction() {} 396 | function emptyFunctionWithReset() {} 397 | emptyFunctionWithReset.resetWarningCache = emptyFunction; 398 | 399 | module.exports = function() { 400 | function shim(props, propName, componentName, location, propFullName, secret) { 401 | if (secret === ReactPropTypesSecret) { 402 | // It is still safe when called from React. 403 | return; 404 | } 405 | var err = new Error( 406 | 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' + 407 | 'Use PropTypes.checkPropTypes() to call them. ' + 408 | 'Read more at http://fb.me/use-check-prop-types' 409 | ); 410 | err.name = 'Invariant Violation'; 411 | throw err; 412 | }; 413 | shim.isRequired = shim; 414 | function getShim() { 415 | return shim; 416 | }; 417 | // Important! 418 | // Keep this list in sync with production version in `./factoryWithTypeCheckers.js`. 419 | var ReactPropTypes = { 420 | array: shim, 421 | bool: shim, 422 | func: shim, 423 | number: shim, 424 | object: shim, 425 | string: shim, 426 | symbol: shim, 427 | 428 | any: shim, 429 | arrayOf: getShim, 430 | element: shim, 431 | elementType: shim, 432 | instanceOf: getShim, 433 | node: shim, 434 | objectOf: getShim, 435 | oneOf: getShim, 436 | oneOfType: getShim, 437 | shape: getShim, 438 | exact: getShim, 439 | 440 | checkPropTypes: emptyFunctionWithReset, 441 | resetWarningCache: emptyFunction 442 | }; 443 | 444 | ReactPropTypes.PropTypes = ReactPropTypes; 445 | 446 | return ReactPropTypes; 447 | }; 448 | 449 | },{"./lib/ReactPropTypesSecret":7}],5:[function(require,module,exports){ 450 | /** 451 | * Copyright (c) 2013-present, Facebook, Inc. 452 | * 453 | * This source code is licensed under the MIT license found in the 454 | * LICENSE file in the root directory of this source tree. 455 | */ 456 | 457 | 'use strict'; 458 | 459 | var ReactIs = require('react-is'); 460 | var assign = require('object-assign'); 461 | 462 | var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); 463 | var checkPropTypes = require('./checkPropTypes'); 464 | 465 | var has = Function.call.bind(Object.prototype.hasOwnProperty); 466 | var printWarning = function() {}; 467 | 468 | if ("production" !== 'production') { 469 | printWarning = function(text) { 470 | var message = 'Warning: ' + text; 471 | if (typeof console !== 'undefined') { 472 | console.error(message); 473 | } 474 | try { 475 | // --- Welcome to debugging React --- 476 | // This error was thrown as a convenience so that you can use this stack 477 | // to find the callsite that caused this warning to fire. 478 | throw new Error(message); 479 | } catch (x) {} 480 | }; 481 | } 482 | 483 | function emptyFunctionThatReturnsNull() { 484 | return null; 485 | } 486 | 487 | module.exports = function(isValidElement, throwOnDirectAccess) { 488 | /* global Symbol */ 489 | var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; 490 | var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. 491 | 492 | /** 493 | * Returns the iterator method function contained on the iterable object. 494 | * 495 | * Be sure to invoke the function with the iterable as context: 496 | * 497 | * var iteratorFn = getIteratorFn(myIterable); 498 | * if (iteratorFn) { 499 | * var iterator = iteratorFn.call(myIterable); 500 | * ... 501 | * } 502 | * 503 | * @param {?object} maybeIterable 504 | * @return {?function} 505 | */ 506 | function getIteratorFn(maybeIterable) { 507 | var iteratorFn = maybeIterable && (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]); 508 | if (typeof iteratorFn === 'function') { 509 | return iteratorFn; 510 | } 511 | } 512 | 513 | /** 514 | * Collection of methods that allow declaration and validation of props that are 515 | * supplied to React components. Example usage: 516 | * 517 | * var Props = require('ReactPropTypes'); 518 | * var MyArticle = React.createClass({ 519 | * propTypes: { 520 | * // An optional string prop named "description". 521 | * description: Props.string, 522 | * 523 | * // A required enum prop named "category". 524 | * category: Props.oneOf(['News','Photos']).isRequired, 525 | * 526 | * // A prop named "dialog" that requires an instance of Dialog. 527 | * dialog: Props.instanceOf(Dialog).isRequired 528 | * }, 529 | * render: function() { ... } 530 | * }); 531 | * 532 | * A more formal specification of how these methods are used: 533 | * 534 | * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...) 535 | * decl := ReactPropTypes.{type}(.isRequired)? 536 | * 537 | * Each and every declaration produces a function with the same signature. This 538 | * allows the creation of custom validation functions. For example: 539 | * 540 | * var MyLink = React.createClass({ 541 | * propTypes: { 542 | * // An optional string or URI prop named "href". 543 | * href: function(props, propName, componentName) { 544 | * var propValue = props[propName]; 545 | * if (propValue != null && typeof propValue !== 'string' && 546 | * !(propValue instanceof URI)) { 547 | * return new Error( 548 | * 'Expected a string or an URI for ' + propName + ' in ' + 549 | * componentName 550 | * ); 551 | * } 552 | * } 553 | * }, 554 | * render: function() {...} 555 | * }); 556 | * 557 | * @internal 558 | */ 559 | 560 | var ANONYMOUS = '<>'; 561 | 562 | // Important! 563 | // Keep this list in sync with production version in `./factoryWithThrowingShims.js`. 564 | var ReactPropTypes = { 565 | array: createPrimitiveTypeChecker('array'), 566 | bool: createPrimitiveTypeChecker('boolean'), 567 | func: createPrimitiveTypeChecker('function'), 568 | number: createPrimitiveTypeChecker('number'), 569 | object: createPrimitiveTypeChecker('object'), 570 | string: createPrimitiveTypeChecker('string'), 571 | symbol: createPrimitiveTypeChecker('symbol'), 572 | 573 | any: createAnyTypeChecker(), 574 | arrayOf: createArrayOfTypeChecker, 575 | element: createElementTypeChecker(), 576 | elementType: createElementTypeTypeChecker(), 577 | instanceOf: createInstanceTypeChecker, 578 | node: createNodeChecker(), 579 | objectOf: createObjectOfTypeChecker, 580 | oneOf: createEnumTypeChecker, 581 | oneOfType: createUnionTypeChecker, 582 | shape: createShapeTypeChecker, 583 | exact: createStrictShapeTypeChecker, 584 | }; 585 | 586 | /** 587 | * inlined Object.is polyfill to avoid requiring consumers ship their own 588 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 589 | */ 590 | /*eslint-disable no-self-compare*/ 591 | function is(x, y) { 592 | // SameValue algorithm 593 | if (x === y) { 594 | // Steps 1-5, 7-10 595 | // Steps 6.b-6.e: +0 != -0 596 | return x !== 0 || 1 / x === 1 / y; 597 | } else { 598 | // Step 6.a: NaN == NaN 599 | return x !== x && y !== y; 600 | } 601 | } 602 | /*eslint-enable no-self-compare*/ 603 | 604 | /** 605 | * We use an Error-like object for backward compatibility as people may call 606 | * PropTypes directly and inspect their output. However, we don't use real 607 | * Errors anymore. We don't inspect their stack anyway, and creating them 608 | * is prohibitively expensive if they are created too often, such as what 609 | * happens in oneOfType() for any type before the one that matched. 610 | */ 611 | function PropTypeError(message) { 612 | this.message = message; 613 | this.stack = ''; 614 | } 615 | // Make `instanceof Error` still work for returned errors. 616 | PropTypeError.prototype = Error.prototype; 617 | 618 | function createChainableTypeChecker(validate) { 619 | if ("production" !== 'production') { 620 | var manualPropTypeCallCache = {}; 621 | var manualPropTypeWarningCount = 0; 622 | } 623 | function checkType(isRequired, props, propName, componentName, location, propFullName, secret) { 624 | componentName = componentName || ANONYMOUS; 625 | propFullName = propFullName || propName; 626 | 627 | if (secret !== ReactPropTypesSecret) { 628 | if (throwOnDirectAccess) { 629 | // New behavior only for users of `prop-types` package 630 | var err = new Error( 631 | 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' + 632 | 'Use `PropTypes.checkPropTypes()` to call them. ' + 633 | 'Read more at http://fb.me/use-check-prop-types' 634 | ); 635 | err.name = 'Invariant Violation'; 636 | throw err; 637 | } else if ("production" !== 'production' && typeof console !== 'undefined') { 638 | // Old behavior for people using React.PropTypes 639 | var cacheKey = componentName + ':' + propName; 640 | if ( 641 | !manualPropTypeCallCache[cacheKey] && 642 | // Avoid spamming the console because they are often not actionable except for lib authors 643 | manualPropTypeWarningCount < 3 644 | ) { 645 | printWarning( 646 | 'You are manually calling a React.PropTypes validation ' + 647 | 'function for the `' + propFullName + '` prop on `' + componentName + '`. This is deprecated ' + 648 | 'and will throw in the standalone `prop-types` package. ' + 649 | 'You may be seeing this warning due to a third-party PropTypes ' + 650 | 'library. See https://fb.me/react-warning-dont-call-proptypes ' + 'for details.' 651 | ); 652 | manualPropTypeCallCache[cacheKey] = true; 653 | manualPropTypeWarningCount++; 654 | } 655 | } 656 | } 657 | if (props[propName] == null) { 658 | if (isRequired) { 659 | if (props[propName] === null) { 660 | return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required ' + ('in `' + componentName + '`, but its value is `null`.')); 661 | } 662 | return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required in ' + ('`' + componentName + '`, but its value is `undefined`.')); 663 | } 664 | return null; 665 | } else { 666 | return validate(props, propName, componentName, location, propFullName); 667 | } 668 | } 669 | 670 | var chainedCheckType = checkType.bind(null, false); 671 | chainedCheckType.isRequired = checkType.bind(null, true); 672 | 673 | return chainedCheckType; 674 | } 675 | 676 | function createPrimitiveTypeChecker(expectedType) { 677 | function validate(props, propName, componentName, location, propFullName, secret) { 678 | var propValue = props[propName]; 679 | var propType = getPropType(propValue); 680 | if (propType !== expectedType) { 681 | // `propValue` being instance of, say, date/regexp, pass the 'object' 682 | // check, but we can offer a more precise error message here rather than 683 | // 'of type `object`'. 684 | var preciseType = getPreciseType(propValue); 685 | 686 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + preciseType + '` supplied to `' + componentName + '`, expected ') + ('`' + expectedType + '`.')); 687 | } 688 | return null; 689 | } 690 | return createChainableTypeChecker(validate); 691 | } 692 | 693 | function createAnyTypeChecker() { 694 | return createChainableTypeChecker(emptyFunctionThatReturnsNull); 695 | } 696 | 697 | function createArrayOfTypeChecker(typeChecker) { 698 | function validate(props, propName, componentName, location, propFullName) { 699 | if (typeof typeChecker !== 'function') { 700 | return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside arrayOf.'); 701 | } 702 | var propValue = props[propName]; 703 | if (!Array.isArray(propValue)) { 704 | var propType = getPropType(propValue); 705 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an array.')); 706 | } 707 | for (var i = 0; i < propValue.length; i++) { 708 | var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']', ReactPropTypesSecret); 709 | if (error instanceof Error) { 710 | return error; 711 | } 712 | } 713 | return null; 714 | } 715 | return createChainableTypeChecker(validate); 716 | } 717 | 718 | function createElementTypeChecker() { 719 | function validate(props, propName, componentName, location, propFullName) { 720 | var propValue = props[propName]; 721 | if (!isValidElement(propValue)) { 722 | var propType = getPropType(propValue); 723 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a single ReactElement.')); 724 | } 725 | return null; 726 | } 727 | return createChainableTypeChecker(validate); 728 | } 729 | 730 | function createElementTypeTypeChecker() { 731 | function validate(props, propName, componentName, location, propFullName) { 732 | var propValue = props[propName]; 733 | if (!ReactIs.isValidElementType(propValue)) { 734 | var propType = getPropType(propValue); 735 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a single ReactElement type.')); 736 | } 737 | return null; 738 | } 739 | return createChainableTypeChecker(validate); 740 | } 741 | 742 | function createInstanceTypeChecker(expectedClass) { 743 | function validate(props, propName, componentName, location, propFullName) { 744 | if (!(props[propName] instanceof expectedClass)) { 745 | var expectedClassName = expectedClass.name || ANONYMOUS; 746 | var actualClassName = getClassName(props[propName]); 747 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + actualClassName + '` supplied to `' + componentName + '`, expected ') + ('instance of `' + expectedClassName + '`.')); 748 | } 749 | return null; 750 | } 751 | return createChainableTypeChecker(validate); 752 | } 753 | 754 | function createEnumTypeChecker(expectedValues) { 755 | if (!Array.isArray(expectedValues)) { 756 | if ("production" !== 'production') { 757 | if (arguments.length > 1) { 758 | printWarning( 759 | 'Invalid arguments supplied to oneOf, expected an array, got ' + arguments.length + ' arguments. ' + 760 | 'A common mistake is to write oneOf(x, y, z) instead of oneOf([x, y, z]).' 761 | ); 762 | } else { 763 | printWarning('Invalid argument supplied to oneOf, expected an array.'); 764 | } 765 | } 766 | return emptyFunctionThatReturnsNull; 767 | } 768 | 769 | function validate(props, propName, componentName, location, propFullName) { 770 | var propValue = props[propName]; 771 | for (var i = 0; i < expectedValues.length; i++) { 772 | if (is(propValue, expectedValues[i])) { 773 | return null; 774 | } 775 | } 776 | 777 | var valuesString = JSON.stringify(expectedValues, function replacer(key, value) { 778 | var type = getPreciseType(value); 779 | if (type === 'symbol') { 780 | return String(value); 781 | } 782 | return value; 783 | }); 784 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of value `' + String(propValue) + '` ' + ('supplied to `' + componentName + '`, expected one of ' + valuesString + '.')); 785 | } 786 | return createChainableTypeChecker(validate); 787 | } 788 | 789 | function createObjectOfTypeChecker(typeChecker) { 790 | function validate(props, propName, componentName, location, propFullName) { 791 | if (typeof typeChecker !== 'function') { 792 | return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside objectOf.'); 793 | } 794 | var propValue = props[propName]; 795 | var propType = getPropType(propValue); 796 | if (propType !== 'object') { 797 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an object.')); 798 | } 799 | for (var key in propValue) { 800 | if (has(propValue, key)) { 801 | var error = typeChecker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); 802 | if (error instanceof Error) { 803 | return error; 804 | } 805 | } 806 | } 807 | return null; 808 | } 809 | return createChainableTypeChecker(validate); 810 | } 811 | 812 | function createUnionTypeChecker(arrayOfTypeCheckers) { 813 | if (!Array.isArray(arrayOfTypeCheckers)) { 814 | "production" !== 'production' ? printWarning('Invalid argument supplied to oneOfType, expected an instance of array.') : void 0; 815 | return emptyFunctionThatReturnsNull; 816 | } 817 | 818 | for (var i = 0; i < arrayOfTypeCheckers.length; i++) { 819 | var checker = arrayOfTypeCheckers[i]; 820 | if (typeof checker !== 'function') { 821 | printWarning( 822 | 'Invalid argument supplied to oneOfType. Expected an array of check functions, but ' + 823 | 'received ' + getPostfixForTypeWarning(checker) + ' at index ' + i + '.' 824 | ); 825 | return emptyFunctionThatReturnsNull; 826 | } 827 | } 828 | 829 | function validate(props, propName, componentName, location, propFullName) { 830 | for (var i = 0; i < arrayOfTypeCheckers.length; i++) { 831 | var checker = arrayOfTypeCheckers[i]; 832 | if (checker(props, propName, componentName, location, propFullName, ReactPropTypesSecret) == null) { 833 | return null; 834 | } 835 | } 836 | 837 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`.')); 838 | } 839 | return createChainableTypeChecker(validate); 840 | } 841 | 842 | function createNodeChecker() { 843 | function validate(props, propName, componentName, location, propFullName) { 844 | if (!isNode(props[propName])) { 845 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a ReactNode.')); 846 | } 847 | return null; 848 | } 849 | return createChainableTypeChecker(validate); 850 | } 851 | 852 | function createShapeTypeChecker(shapeTypes) { 853 | function validate(props, propName, componentName, location, propFullName) { 854 | var propValue = props[propName]; 855 | var propType = getPropType(propValue); 856 | if (propType !== 'object') { 857 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.')); 858 | } 859 | for (var key in shapeTypes) { 860 | var checker = shapeTypes[key]; 861 | if (!checker) { 862 | continue; 863 | } 864 | var error = checker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); 865 | if (error) { 866 | return error; 867 | } 868 | } 869 | return null; 870 | } 871 | return createChainableTypeChecker(validate); 872 | } 873 | 874 | function createStrictShapeTypeChecker(shapeTypes) { 875 | function validate(props, propName, componentName, location, propFullName) { 876 | var propValue = props[propName]; 877 | var propType = getPropType(propValue); 878 | if (propType !== 'object') { 879 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.')); 880 | } 881 | // We need to check all keys in case some are required but missing from 882 | // props. 883 | var allKeys = assign({}, props[propName], shapeTypes); 884 | for (var key in allKeys) { 885 | var checker = shapeTypes[key]; 886 | if (!checker) { 887 | return new PropTypeError( 888 | 'Invalid ' + location + ' `' + propFullName + '` key `' + key + '` supplied to `' + componentName + '`.' + 889 | '\nBad object: ' + JSON.stringify(props[propName], null, ' ') + 890 | '\nValid keys: ' + JSON.stringify(Object.keys(shapeTypes), null, ' ') 891 | ); 892 | } 893 | var error = checker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); 894 | if (error) { 895 | return error; 896 | } 897 | } 898 | return null; 899 | } 900 | 901 | return createChainableTypeChecker(validate); 902 | } 903 | 904 | function isNode(propValue) { 905 | switch (typeof propValue) { 906 | case 'number': 907 | case 'string': 908 | case 'undefined': 909 | return true; 910 | case 'boolean': 911 | return !propValue; 912 | case 'object': 913 | if (Array.isArray(propValue)) { 914 | return propValue.every(isNode); 915 | } 916 | if (propValue === null || isValidElement(propValue)) { 917 | return true; 918 | } 919 | 920 | var iteratorFn = getIteratorFn(propValue); 921 | if (iteratorFn) { 922 | var iterator = iteratorFn.call(propValue); 923 | var step; 924 | if (iteratorFn !== propValue.entries) { 925 | while (!(step = iterator.next()).done) { 926 | if (!isNode(step.value)) { 927 | return false; 928 | } 929 | } 930 | } else { 931 | // Iterator will provide entry [k,v] tuples rather than values. 932 | while (!(step = iterator.next()).done) { 933 | var entry = step.value; 934 | if (entry) { 935 | if (!isNode(entry[1])) { 936 | return false; 937 | } 938 | } 939 | } 940 | } 941 | } else { 942 | return false; 943 | } 944 | 945 | return true; 946 | default: 947 | return false; 948 | } 949 | } 950 | 951 | function isSymbol(propType, propValue) { 952 | // Native Symbol. 953 | if (propType === 'symbol') { 954 | return true; 955 | } 956 | 957 | // falsy value can't be a Symbol 958 | if (!propValue) { 959 | return false; 960 | } 961 | 962 | // 19.4.3.5 Symbol.prototype[@@toStringTag] === 'Symbol' 963 | if (propValue['@@toStringTag'] === 'Symbol') { 964 | return true; 965 | } 966 | 967 | // Fallback for non-spec compliant Symbols which are polyfilled. 968 | if (typeof Symbol === 'function' && propValue instanceof Symbol) { 969 | return true; 970 | } 971 | 972 | return false; 973 | } 974 | 975 | // Equivalent of `typeof` but with special handling for array and regexp. 976 | function getPropType(propValue) { 977 | var propType = typeof propValue; 978 | if (Array.isArray(propValue)) { 979 | return 'array'; 980 | } 981 | if (propValue instanceof RegExp) { 982 | // Old webkits (at least until Android 4.0) return 'function' rather than 983 | // 'object' for typeof a RegExp. We'll normalize this here so that /bla/ 984 | // passes PropTypes.object. 985 | return 'object'; 986 | } 987 | if (isSymbol(propType, propValue)) { 988 | return 'symbol'; 989 | } 990 | return propType; 991 | } 992 | 993 | // This handles more types than `getPropType`. Only used for error messages. 994 | // See `createPrimitiveTypeChecker`. 995 | function getPreciseType(propValue) { 996 | if (typeof propValue === 'undefined' || propValue === null) { 997 | return '' + propValue; 998 | } 999 | var propType = getPropType(propValue); 1000 | if (propType === 'object') { 1001 | if (propValue instanceof Date) { 1002 | return 'date'; 1003 | } else if (propValue instanceof RegExp) { 1004 | return 'regexp'; 1005 | } 1006 | } 1007 | return propType; 1008 | } 1009 | 1010 | // Returns a string that is postfixed to a warning about an invalid type. 1011 | // For example, "undefined" or "of type array" 1012 | function getPostfixForTypeWarning(value) { 1013 | var type = getPreciseType(value); 1014 | switch (type) { 1015 | case 'array': 1016 | case 'object': 1017 | return 'an ' + type; 1018 | case 'boolean': 1019 | case 'date': 1020 | case 'regexp': 1021 | return 'a ' + type; 1022 | default: 1023 | return type; 1024 | } 1025 | } 1026 | 1027 | // Returns class name of the object, if any. 1028 | function getClassName(propValue) { 1029 | if (!propValue.constructor || !propValue.constructor.name) { 1030 | return ANONYMOUS; 1031 | } 1032 | return propValue.constructor.name; 1033 | } 1034 | 1035 | ReactPropTypes.checkPropTypes = checkPropTypes; 1036 | ReactPropTypes.resetWarningCache = checkPropTypes.resetWarningCache; 1037 | ReactPropTypes.PropTypes = ReactPropTypes; 1038 | 1039 | return ReactPropTypes; 1040 | }; 1041 | 1042 | },{"./checkPropTypes":3,"./lib/ReactPropTypesSecret":7,"object-assign":1,"react-is":11}],6:[function(require,module,exports){ 1043 | /** 1044 | * Copyright (c) 2013-present, Facebook, Inc. 1045 | * 1046 | * This source code is licensed under the MIT license found in the 1047 | * LICENSE file in the root directory of this source tree. 1048 | */ 1049 | 1050 | if ("production" !== 'production') { 1051 | var ReactIs = require('react-is'); 1052 | 1053 | // By explicitly using `prop-types` you are opting into new development behavior. 1054 | // http://fb.me/prop-types-in-prod 1055 | var throwOnDirectAccess = true; 1056 | module.exports = require('./factoryWithTypeCheckers')(ReactIs.isElement, throwOnDirectAccess); 1057 | } else { 1058 | // By explicitly using `prop-types` you are opting into new production behavior. 1059 | // http://fb.me/prop-types-in-prod 1060 | module.exports = require('./factoryWithThrowingShims')(); 1061 | } 1062 | 1063 | },{"./factoryWithThrowingShims":4,"./factoryWithTypeCheckers":5,"react-is":11}],7:[function(require,module,exports){ 1064 | /** 1065 | * Copyright (c) 2013-present, Facebook, Inc. 1066 | * 1067 | * This source code is licensed under the MIT license found in the 1068 | * LICENSE file in the root directory of this source tree. 1069 | */ 1070 | 1071 | 'use strict'; 1072 | 1073 | var ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; 1074 | 1075 | module.exports = ReactPropTypesSecret; 1076 | 1077 | },{}],8:[function(require,module,exports){ 1078 | 'use strict'; 1079 | 1080 | var isArray = Array.isArray; 1081 | var keyList = Object.keys; 1082 | var hasProp = Object.prototype.hasOwnProperty; 1083 | var hasElementType = typeof Element !== 'undefined'; 1084 | 1085 | function equal(a, b) { 1086 | // fast-deep-equal index.js 2.0.1 1087 | if (a === b) return true; 1088 | 1089 | if (a && b && typeof a == 'object' && typeof b == 'object') { 1090 | var arrA = isArray(a) 1091 | , arrB = isArray(b) 1092 | , i 1093 | , length 1094 | , key; 1095 | 1096 | if (arrA && arrB) { 1097 | length = a.length; 1098 | if (length != b.length) return false; 1099 | for (i = length; i-- !== 0;) 1100 | if (!equal(a[i], b[i])) return false; 1101 | return true; 1102 | } 1103 | 1104 | if (arrA != arrB) return false; 1105 | 1106 | var dateA = a instanceof Date 1107 | , dateB = b instanceof Date; 1108 | if (dateA != dateB) return false; 1109 | if (dateA && dateB) return a.getTime() == b.getTime(); 1110 | 1111 | var regexpA = a instanceof RegExp 1112 | , regexpB = b instanceof RegExp; 1113 | if (regexpA != regexpB) return false; 1114 | if (regexpA && regexpB) return a.toString() == b.toString(); 1115 | 1116 | var keys = keyList(a); 1117 | length = keys.length; 1118 | 1119 | if (length !== keyList(b).length) 1120 | return false; 1121 | 1122 | for (i = length; i-- !== 0;) 1123 | if (!hasProp.call(b, keys[i])) return false; 1124 | // end fast-deep-equal 1125 | 1126 | // start react-fast-compare 1127 | // custom handling for DOM elements 1128 | if (hasElementType && a instanceof Element && b instanceof Element) 1129 | return a === b; 1130 | 1131 | // custom handling for React 1132 | for (i = length; i-- !== 0;) { 1133 | key = keys[i]; 1134 | if (key === '_owner' && a.$$typeof) { 1135 | // React-specific: avoid traversing React elements' _owner. 1136 | // _owner contains circular references 1137 | // and is not needed when comparing the actual elements (and not their owners) 1138 | // .$$typeof and ._store on just reasonable markers of a react element 1139 | continue; 1140 | } else { 1141 | // all other properties should be traversed as usual 1142 | if (!equal(a[key], b[key])) return false; 1143 | } 1144 | } 1145 | // end react-fast-compare 1146 | 1147 | // fast-deep-equal index.js 2.0.1 1148 | return true; 1149 | } 1150 | 1151 | return a !== a && b !== b; 1152 | } 1153 | // end fast-deep-equal 1154 | 1155 | module.exports = function exportedEqual(a, b) { 1156 | try { 1157 | return equal(a, b); 1158 | } catch (error) { 1159 | if ((error.message && error.message.match(/stack|recursion/i)) || (error.number === -2146828260)) { 1160 | // warn on circular references, don't crash 1161 | // browsers give this different errors name and messages: 1162 | // chrome/safari: "RangeError", "Maximum call stack size exceeded" 1163 | // firefox: "InternalError", too much recursion" 1164 | // edge: "Error", "Out of stack space" 1165 | console.warn('Warning: react-fast-compare does not handle circular references.', error.name, error.message); 1166 | return false; 1167 | } 1168 | // some other error. we should definitely know about these 1169 | throw error; 1170 | } 1171 | }; 1172 | 1173 | },{}],9:[function(require,module,exports){ 1174 | (function (process){ 1175 | /** @license React v16.9.0 1176 | * react-is.development.js 1177 | * 1178 | * Copyright (c) Facebook, Inc. and its affiliates. 1179 | * 1180 | * This source code is licensed under the MIT license found in the 1181 | * LICENSE file in the root directory of this source tree. 1182 | */ 1183 | 1184 | 'use strict'; 1185 | 1186 | 1187 | 1188 | if (process.env.NODE_ENV !== "production") { 1189 | (function() { 1190 | 'use strict'; 1191 | 1192 | Object.defineProperty(exports, '__esModule', { value: true }); 1193 | 1194 | // The Symbol used to tag the ReactElement-like types. If there is no native Symbol 1195 | // nor polyfill, then a plain number is used for performance. 1196 | var hasSymbol = typeof Symbol === 'function' && Symbol.for; 1197 | 1198 | var REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7; 1199 | var REACT_PORTAL_TYPE = hasSymbol ? Symbol.for('react.portal') : 0xeaca; 1200 | var REACT_FRAGMENT_TYPE = hasSymbol ? Symbol.for('react.fragment') : 0xeacb; 1201 | var REACT_STRICT_MODE_TYPE = hasSymbol ? Symbol.for('react.strict_mode') : 0xeacc; 1202 | var REACT_PROFILER_TYPE = hasSymbol ? Symbol.for('react.profiler') : 0xead2; 1203 | var REACT_PROVIDER_TYPE = hasSymbol ? Symbol.for('react.provider') : 0xeacd; 1204 | var REACT_CONTEXT_TYPE = hasSymbol ? Symbol.for('react.context') : 0xeace; 1205 | // TODO: We don't use AsyncMode or ConcurrentMode anymore. They were temporary 1206 | // (unstable) APIs that have been removed. Can we remove the symbols? 1207 | var REACT_ASYNC_MODE_TYPE = hasSymbol ? Symbol.for('react.async_mode') : 0xeacf; 1208 | var REACT_CONCURRENT_MODE_TYPE = hasSymbol ? Symbol.for('react.concurrent_mode') : 0xeacf; 1209 | var REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0; 1210 | var REACT_SUSPENSE_TYPE = hasSymbol ? Symbol.for('react.suspense') : 0xead1; 1211 | var REACT_SUSPENSE_LIST_TYPE = hasSymbol ? Symbol.for('react.suspense_list') : 0xead8; 1212 | var REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3; 1213 | var REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4; 1214 | var REACT_FUNDAMENTAL_TYPE = hasSymbol ? Symbol.for('react.fundamental') : 0xead5; 1215 | var REACT_RESPONDER_TYPE = hasSymbol ? Symbol.for('react.responder') : 0xead6; 1216 | 1217 | function isValidElementType(type) { 1218 | return typeof type === 'string' || typeof type === 'function' || 1219 | // Note: its typeof might be other than 'symbol' or 'number' if it's a polyfill. 1220 | type === REACT_FRAGMENT_TYPE || type === REACT_CONCURRENT_MODE_TYPE || type === REACT_PROFILER_TYPE || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || type === REACT_SUSPENSE_LIST_TYPE || typeof type === 'object' && type !== null && (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || type.$$typeof === REACT_FUNDAMENTAL_TYPE || type.$$typeof === REACT_RESPONDER_TYPE); 1221 | } 1222 | 1223 | /** 1224 | * Forked from fbjs/warning: 1225 | * https://github.com/facebook/fbjs/blob/e66ba20ad5be433eb54423f2b097d829324d9de6/packages/fbjs/src/__forks__/warning.js 1226 | * 1227 | * Only change is we use console.warn instead of console.error, 1228 | * and do nothing when 'console' is not supported. 1229 | * This really simplifies the code. 1230 | * --- 1231 | * Similar to invariant but only logs a warning if the condition is not met. 1232 | * This can be used to log issues in development environments in critical 1233 | * paths. Removing the logging code for production environments will keep the 1234 | * same logic and follow the same code paths. 1235 | */ 1236 | 1237 | var lowPriorityWarning = function () {}; 1238 | 1239 | { 1240 | var printWarning = function (format) { 1241 | for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 1242 | args[_key - 1] = arguments[_key]; 1243 | } 1244 | 1245 | var argIndex = 0; 1246 | var message = 'Warning: ' + format.replace(/%s/g, function () { 1247 | return args[argIndex++]; 1248 | }); 1249 | if (typeof console !== 'undefined') { 1250 | console.warn(message); 1251 | } 1252 | try { 1253 | // --- Welcome to debugging React --- 1254 | // This error was thrown as a convenience so that you can use this stack 1255 | // to find the callsite that caused this warning to fire. 1256 | throw new Error(message); 1257 | } catch (x) {} 1258 | }; 1259 | 1260 | lowPriorityWarning = function (condition, format) { 1261 | if (format === undefined) { 1262 | throw new Error('`lowPriorityWarning(condition, format, ...args)` requires a warning ' + 'message argument'); 1263 | } 1264 | if (!condition) { 1265 | for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { 1266 | args[_key2 - 2] = arguments[_key2]; 1267 | } 1268 | 1269 | printWarning.apply(undefined, [format].concat(args)); 1270 | } 1271 | }; 1272 | } 1273 | 1274 | var lowPriorityWarning$1 = lowPriorityWarning; 1275 | 1276 | function typeOf(object) { 1277 | if (typeof object === 'object' && object !== null) { 1278 | var $$typeof = object.$$typeof; 1279 | switch ($$typeof) { 1280 | case REACT_ELEMENT_TYPE: 1281 | var type = object.type; 1282 | 1283 | switch (type) { 1284 | case REACT_ASYNC_MODE_TYPE: 1285 | case REACT_CONCURRENT_MODE_TYPE: 1286 | case REACT_FRAGMENT_TYPE: 1287 | case REACT_PROFILER_TYPE: 1288 | case REACT_STRICT_MODE_TYPE: 1289 | case REACT_SUSPENSE_TYPE: 1290 | return type; 1291 | default: 1292 | var $$typeofType = type && type.$$typeof; 1293 | 1294 | switch ($$typeofType) { 1295 | case REACT_CONTEXT_TYPE: 1296 | case REACT_FORWARD_REF_TYPE: 1297 | case REACT_PROVIDER_TYPE: 1298 | return $$typeofType; 1299 | default: 1300 | return $$typeof; 1301 | } 1302 | } 1303 | case REACT_LAZY_TYPE: 1304 | case REACT_MEMO_TYPE: 1305 | case REACT_PORTAL_TYPE: 1306 | return $$typeof; 1307 | } 1308 | } 1309 | 1310 | return undefined; 1311 | } 1312 | 1313 | // AsyncMode is deprecated along with isAsyncMode 1314 | var AsyncMode = REACT_ASYNC_MODE_TYPE; 1315 | var ConcurrentMode = REACT_CONCURRENT_MODE_TYPE; 1316 | var ContextConsumer = REACT_CONTEXT_TYPE; 1317 | var ContextProvider = REACT_PROVIDER_TYPE; 1318 | var Element = REACT_ELEMENT_TYPE; 1319 | var ForwardRef = REACT_FORWARD_REF_TYPE; 1320 | var Fragment = REACT_FRAGMENT_TYPE; 1321 | var Lazy = REACT_LAZY_TYPE; 1322 | var Memo = REACT_MEMO_TYPE; 1323 | var Portal = REACT_PORTAL_TYPE; 1324 | var Profiler = REACT_PROFILER_TYPE; 1325 | var StrictMode = REACT_STRICT_MODE_TYPE; 1326 | var Suspense = REACT_SUSPENSE_TYPE; 1327 | 1328 | var hasWarnedAboutDeprecatedIsAsyncMode = false; 1329 | 1330 | // AsyncMode should be deprecated 1331 | function isAsyncMode(object) { 1332 | { 1333 | if (!hasWarnedAboutDeprecatedIsAsyncMode) { 1334 | hasWarnedAboutDeprecatedIsAsyncMode = true; 1335 | lowPriorityWarning$1(false, 'The ReactIs.isAsyncMode() alias has been deprecated, ' + 'and will be removed in React 17+. Update your code to use ' + 'ReactIs.isConcurrentMode() instead. It has the exact same API.'); 1336 | } 1337 | } 1338 | return isConcurrentMode(object) || typeOf(object) === REACT_ASYNC_MODE_TYPE; 1339 | } 1340 | function isConcurrentMode(object) { 1341 | return typeOf(object) === REACT_CONCURRENT_MODE_TYPE; 1342 | } 1343 | function isContextConsumer(object) { 1344 | return typeOf(object) === REACT_CONTEXT_TYPE; 1345 | } 1346 | function isContextProvider(object) { 1347 | return typeOf(object) === REACT_PROVIDER_TYPE; 1348 | } 1349 | function isElement(object) { 1350 | return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE; 1351 | } 1352 | function isForwardRef(object) { 1353 | return typeOf(object) === REACT_FORWARD_REF_TYPE; 1354 | } 1355 | function isFragment(object) { 1356 | return typeOf(object) === REACT_FRAGMENT_TYPE; 1357 | } 1358 | function isLazy(object) { 1359 | return typeOf(object) === REACT_LAZY_TYPE; 1360 | } 1361 | function isMemo(object) { 1362 | return typeOf(object) === REACT_MEMO_TYPE; 1363 | } 1364 | function isPortal(object) { 1365 | return typeOf(object) === REACT_PORTAL_TYPE; 1366 | } 1367 | function isProfiler(object) { 1368 | return typeOf(object) === REACT_PROFILER_TYPE; 1369 | } 1370 | function isStrictMode(object) { 1371 | return typeOf(object) === REACT_STRICT_MODE_TYPE; 1372 | } 1373 | function isSuspense(object) { 1374 | return typeOf(object) === REACT_SUSPENSE_TYPE; 1375 | } 1376 | 1377 | exports.typeOf = typeOf; 1378 | exports.AsyncMode = AsyncMode; 1379 | exports.ConcurrentMode = ConcurrentMode; 1380 | exports.ContextConsumer = ContextConsumer; 1381 | exports.ContextProvider = ContextProvider; 1382 | exports.Element = Element; 1383 | exports.ForwardRef = ForwardRef; 1384 | exports.Fragment = Fragment; 1385 | exports.Lazy = Lazy; 1386 | exports.Memo = Memo; 1387 | exports.Portal = Portal; 1388 | exports.Profiler = Profiler; 1389 | exports.StrictMode = StrictMode; 1390 | exports.Suspense = Suspense; 1391 | exports.isValidElementType = isValidElementType; 1392 | exports.isAsyncMode = isAsyncMode; 1393 | exports.isConcurrentMode = isConcurrentMode; 1394 | exports.isContextConsumer = isContextConsumer; 1395 | exports.isContextProvider = isContextProvider; 1396 | exports.isElement = isElement; 1397 | exports.isForwardRef = isForwardRef; 1398 | exports.isFragment = isFragment; 1399 | exports.isLazy = isLazy; 1400 | exports.isMemo = isMemo; 1401 | exports.isPortal = isPortal; 1402 | exports.isProfiler = isProfiler; 1403 | exports.isStrictMode = isStrictMode; 1404 | exports.isSuspense = isSuspense; 1405 | })(); 1406 | } 1407 | 1408 | }).call(this,require('_process')) 1409 | },{"_process":2}],10:[function(require,module,exports){ 1410 | /** @license React v16.9.0 1411 | * react-is.production.min.js 1412 | * 1413 | * Copyright (c) Facebook, Inc. and its affiliates. 1414 | * 1415 | * This source code is licensed under the MIT license found in the 1416 | * LICENSE file in the root directory of this source tree. 1417 | */ 1418 | 1419 | 'use strict';Object.defineProperty(exports,"__esModule",{value:!0}); 1420 | var b="function"===typeof Symbol&&Symbol.for,c=b?Symbol.for("react.element"):60103,d=b?Symbol.for("react.portal"):60106,e=b?Symbol.for("react.fragment"):60107,f=b?Symbol.for("react.strict_mode"):60108,g=b?Symbol.for("react.profiler"):60114,h=b?Symbol.for("react.provider"):60109,k=b?Symbol.for("react.context"):60110,l=b?Symbol.for("react.async_mode"):60111,m=b?Symbol.for("react.concurrent_mode"):60111,n=b?Symbol.for("react.forward_ref"):60112,p=b?Symbol.for("react.suspense"):60113,q=b?Symbol.for("react.suspense_list"): 1421 | 60120,r=b?Symbol.for("react.memo"):60115,t=b?Symbol.for("react.lazy"):60116,v=b?Symbol.for("react.fundamental"):60117,w=b?Symbol.for("react.responder"):60118;function x(a){if("object"===typeof a&&null!==a){var u=a.$$typeof;switch(u){case c:switch(a=a.type,a){case l:case m:case e:case g:case f:case p:return a;default:switch(a=a&&a.$$typeof,a){case k:case n:case h:return a;default:return u}}case t:case r:case d:return u}}}function y(a){return x(a)===m}exports.typeOf=x;exports.AsyncMode=l; 1422 | exports.ConcurrentMode=m;exports.ContextConsumer=k;exports.ContextProvider=h;exports.Element=c;exports.ForwardRef=n;exports.Fragment=e;exports.Lazy=t;exports.Memo=r;exports.Portal=d;exports.Profiler=g;exports.StrictMode=f;exports.Suspense=p; 1423 | exports.isValidElementType=function(a){return"string"===typeof a||"function"===typeof a||a===e||a===m||a===g||a===f||a===p||a===q||"object"===typeof a&&null!==a&&(a.$$typeof===t||a.$$typeof===r||a.$$typeof===h||a.$$typeof===k||a.$$typeof===n||a.$$typeof===v||a.$$typeof===w)};exports.isAsyncMode=function(a){return y(a)||x(a)===l};exports.isConcurrentMode=y;exports.isContextConsumer=function(a){return x(a)===k};exports.isContextProvider=function(a){return x(a)===h}; 1424 | exports.isElement=function(a){return"object"===typeof a&&null!==a&&a.$$typeof===c};exports.isForwardRef=function(a){return x(a)===n};exports.isFragment=function(a){return x(a)===e};exports.isLazy=function(a){return x(a)===t};exports.isMemo=function(a){return x(a)===r};exports.isPortal=function(a){return x(a)===d};exports.isProfiler=function(a){return x(a)===g};exports.isStrictMode=function(a){return x(a)===f};exports.isSuspense=function(a){return x(a)===p}; 1425 | 1426 | },{}],11:[function(require,module,exports){ 1427 | (function (process){ 1428 | 'use strict'; 1429 | 1430 | if (process.env.NODE_ENV === 'production') { 1431 | module.exports = require('./cjs/react-is.production.min.js'); 1432 | } else { 1433 | module.exports = require('./cjs/react-is.development.js'); 1434 | } 1435 | 1436 | }).call(this,require('_process')) 1437 | },{"./cjs/react-is.development.js":9,"./cjs/react-is.production.min.js":10,"_process":2}],12:[function(require,module,exports){ 1438 | (function (global){ 1439 | "use strict"; 1440 | 1441 | Object.defineProperty(exports, "__esModule", { 1442 | value: true 1443 | }); 1444 | 1445 | var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })(); 1446 | 1447 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 1448 | 1449 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 1450 | 1451 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 1452 | 1453 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 1454 | 1455 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } 1456 | 1457 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1458 | 1459 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 1460 | 1461 | var _react = (typeof window !== "undefined" ? window['React'] : typeof global !== "undefined" ? global['React'] : null); 1462 | 1463 | var _react2 = _interopRequireDefault(_react); 1464 | 1465 | var _propTypes = require("prop-types"); 1466 | 1467 | var _propTypes2 = _interopRequireDefault(_propTypes); 1468 | 1469 | var _reactFastCompare = require("react-fast-compare"); 1470 | 1471 | var _reactFastCompare2 = _interopRequireDefault(_reactFastCompare); 1472 | 1473 | var ImageMapper = (function (_Component) { 1474 | _inherits(ImageMapper, _Component); 1475 | 1476 | function ImageMapper(props) { 1477 | var _this = this; 1478 | 1479 | _classCallCheck(this, ImageMapper); 1480 | 1481 | _get(Object.getPrototypeOf(ImageMapper.prototype), "constructor", this).call(this, props); 1482 | ["drawrect", "drawcircle", "drawpoly", "initCanvas", "renderPrefilledAreas"].forEach(function (f) { 1483 | return _this[f] = _this[f].bind(_this); 1484 | }); 1485 | var absPos = { position: "absolute", top: 0, left: 0 }; 1486 | this.styles = { 1487 | container: { position: "relative" }, 1488 | canvas: _extends({}, absPos, { pointerEvents: "none", zIndex: 2 }), 1489 | img: _extends({}, absPos, { zIndex: 1, userSelect: "none" }), 1490 | map: props.onClick && { cursor: "pointer" } || undefined 1491 | }; 1492 | // Props watched for changes to trigger update 1493 | this.watchedProps = ["active", "fillColor", "height", "imgWidth", "lineWidth", "src", "strokeColor", "width"]; 1494 | } 1495 | 1496 | _createClass(ImageMapper, [{ 1497 | key: "shouldComponentUpdate", 1498 | value: function shouldComponentUpdate(nextProps) { 1499 | var _this2 = this; 1500 | 1501 | var propChanged = this.watchedProps.some(function (prop) { 1502 | return _this2.props[prop] !== nextProps[prop]; 1503 | }); 1504 | return !(0, _reactFastCompare2["default"])(this.props.map, this.state.map) || propChanged; 1505 | } 1506 | }, { 1507 | key: "componentWillMount", 1508 | value: function componentWillMount() { 1509 | this.updateCacheMap(); 1510 | } 1511 | }, { 1512 | key: "updateCacheMap", 1513 | value: function updateCacheMap() { 1514 | this.setState({ map: JSON.parse(JSON.stringify(this.props.map)) }, this.initCanvas); 1515 | } 1516 | }, { 1517 | key: "componentDidUpdate", 1518 | value: function componentDidUpdate() { 1519 | this.updateCacheMap(); 1520 | this.initCanvas(); 1521 | } 1522 | }, { 1523 | key: "drawrect", 1524 | value: function drawrect(coords, fillColor, lineWidth, strokeColor) { 1525 | var _coords = _slicedToArray(coords, 4); 1526 | 1527 | var left = _coords[0]; 1528 | var top = _coords[1]; 1529 | var right = _coords[2]; 1530 | var bot = _coords[3]; 1531 | 1532 | this.ctx.fillStyle = fillColor; 1533 | this.ctx.lineWidth = lineWidth; 1534 | this.ctx.strokeStyle = strokeColor; 1535 | this.ctx.strokeRect(left, top, right - left, bot - top); 1536 | this.ctx.fillRect(left, top, right - left, bot - top); 1537 | this.ctx.fillStyle = this.props.fillColor; 1538 | } 1539 | }, { 1540 | key: "drawcircle", 1541 | value: function drawcircle(coords, fillColor, lineWidth, strokeColor) { 1542 | this.ctx.fillStyle = fillColor; 1543 | this.ctx.beginPath(); 1544 | this.ctx.lineWidth = lineWidth; 1545 | this.ctx.strokeStyle = strokeColor; 1546 | this.ctx.arc(coords[0], coords[1], coords[2], 0, 2 * Math.PI); 1547 | this.ctx.closePath(); 1548 | this.ctx.stroke(); 1549 | this.ctx.fill(); 1550 | this.ctx.fillStyle = this.props.fillColor; 1551 | } 1552 | }, { 1553 | key: "drawpoly", 1554 | value: function drawpoly(coords, fillColor, lineWidth, strokeColor) { 1555 | var _this3 = this; 1556 | 1557 | coords = coords.reduce(function (a, v, i, s) { 1558 | return i % 2 ? a : [].concat(_toConsumableArray(a), [s.slice(i, i + 2)]); 1559 | }, []); 1560 | 1561 | this.ctx.fillStyle = fillColor; 1562 | this.ctx.beginPath(); 1563 | this.ctx.lineWidth = lineWidth; 1564 | this.ctx.strokeStyle = strokeColor; 1565 | var first = coords.unshift(); 1566 | this.ctx.moveTo(first[0], first[1]); 1567 | coords.forEach(function (c) { 1568 | return _this3.ctx.lineTo(c[0], c[1]); 1569 | }); 1570 | this.ctx.closePath(); 1571 | this.ctx.stroke(); 1572 | this.ctx.fill(); 1573 | this.ctx.fillStyle = this.props.fillColor; 1574 | } 1575 | }, { 1576 | key: "initCanvas", 1577 | value: function initCanvas() { 1578 | if (this.props.width) this.img.width = this.props.width; 1579 | 1580 | if (this.props.height) this.img.height = this.props.height; 1581 | 1582 | this.canvas.width = this.props.width || this.img.clientWidth; 1583 | this.canvas.height = this.props.height || this.img.clientHeight; 1584 | this.container.style.width = (this.props.width || this.img.clientWidth) + "px"; 1585 | this.container.style.height = (this.props.height || this.img.clientHeight) + "px"; 1586 | this.ctx = this.canvas.getContext("2d"); 1587 | this.ctx.fillStyle = this.props.fillColor; 1588 | //this.ctx.strokeStyle = this.props.strokeColor; 1589 | 1590 | if (this.props.onLoad) this.props.onLoad(); 1591 | 1592 | this.renderPrefilledAreas(); 1593 | } 1594 | }, { 1595 | key: "hoverOn", 1596 | value: function hoverOn(area, index, event) { 1597 | var shape = event.target.getAttribute("shape"); 1598 | 1599 | if (this.props.active && this["draw" + shape]) { 1600 | this["draw" + shape](event.target.getAttribute("coords").split(","), area.fillColor, area.lineWidth || this.props.lineWidth, area.strokeColor || this.props.strokeColor); 1601 | } 1602 | if (this.props.onMouseEnter) this.props.onMouseEnter(area, index, event); 1603 | } 1604 | }, { 1605 | key: "hoverOff", 1606 | value: function hoverOff(area, index, event) { 1607 | if (this.props.active) { 1608 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 1609 | this.renderPrefilledAreas(); 1610 | } 1611 | 1612 | if (this.props.onMouseLeave) this.props.onMouseLeave(area, index, event); 1613 | } 1614 | }, { 1615 | key: "click", 1616 | value: function click(area, index, event) { 1617 | if (this.props.onClick) { 1618 | event.preventDefault(); 1619 | this.props.onClick(area, index, event); 1620 | } 1621 | } 1622 | }, { 1623 | key: "imageClick", 1624 | value: function imageClick(event) { 1625 | if (this.props.onImageClick) { 1626 | event.preventDefault(); 1627 | this.props.onImageClick(event); 1628 | } 1629 | } 1630 | }, { 1631 | key: "mouseMove", 1632 | value: function mouseMove(area, index, event) { 1633 | if (this.props.onMouseMove) { 1634 | this.props.onMouseMove(area, index, event); 1635 | } 1636 | } 1637 | }, { 1638 | key: "imageMouseMove", 1639 | value: function imageMouseMove(area, index, event) { 1640 | if (this.props.onImageMouseMove) { 1641 | this.props.onImageMouseMove(area, index, event); 1642 | } 1643 | } 1644 | }, { 1645 | key: "scaleCoords", 1646 | value: function scaleCoords(coords) { 1647 | var _props = this.props; 1648 | var imgWidth = _props.imgWidth; 1649 | var width = _props.width; 1650 | 1651 | // calculate scale based on current 'width' and the original 'imgWidth' 1652 | var scale = width && imgWidth && imgWidth > 0 ? width / imgWidth : 1; 1653 | return coords.map(function (coord) { 1654 | return coord * scale; 1655 | }); 1656 | } 1657 | }, { 1658 | key: "renderPrefilledAreas", 1659 | value: function renderPrefilledAreas() { 1660 | var _this4 = this; 1661 | 1662 | this.state.map.areas.map(function (area) { 1663 | if (!area.preFillColor) return; 1664 | _this4["draw" + area.shape](_this4.scaleCoords(area.coords), area.preFillColor, area.lineWidth || _this4.props.lineWidth, area.strokeColor || _this4.props.strokeColor); 1665 | }); 1666 | } 1667 | }, { 1668 | key: "computeCenter", 1669 | value: function computeCenter(area) { 1670 | if (!area) return [0, 0]; 1671 | 1672 | var scaledCoords = this.scaleCoords(area.coords); 1673 | 1674 | switch (area.shape) { 1675 | case "circle": 1676 | return [scaledCoords[0], scaledCoords[1]]; 1677 | case "poly": 1678 | case "rect": 1679 | default: 1680 | { 1681 | var _ret = (function () { 1682 | // Calculate centroid 1683 | var n = scaledCoords.length / 2; 1684 | 1685 | var _scaledCoords$reduce = scaledCoords.reduce(function (_ref, val, idx) { 1686 | var y = _ref.y; 1687 | var x = _ref.x; 1688 | 1689 | return !(idx % 2) ? { y: y, x: x + val / n } : { y: y + val / n, x: x }; 1690 | }, { y: 0, x: 0 }); 1691 | 1692 | var y = _scaledCoords$reduce.y; 1693 | var x = _scaledCoords$reduce.x; 1694 | 1695 | return { 1696 | v: [x, y] 1697 | }; 1698 | })(); 1699 | 1700 | if (typeof _ret === "object") return _ret.v; 1701 | } 1702 | } 1703 | } 1704 | }, { 1705 | key: "renderAreas", 1706 | value: function renderAreas() { 1707 | var _this5 = this; 1708 | 1709 | return this.state.map.areas.map(function (area, index) { 1710 | var scaledCoords = _this5.scaleCoords(area.coords); 1711 | var center = _this5.computeCenter(area); 1712 | var extendedArea = _extends({}, area, { scaledCoords: scaledCoords, center: center }); 1713 | return _react2["default"].createElement("area", { 1714 | key: area._id || index, 1715 | shape: area.shape, 1716 | coords: scaledCoords.join(","), 1717 | onMouseEnter: _this5.hoverOn.bind(_this5, extendedArea, index), 1718 | onMouseLeave: _this5.hoverOff.bind(_this5, extendedArea, index), 1719 | onMouseMove: _this5.mouseMove.bind(_this5, extendedArea, index), 1720 | onClick: _this5.click.bind(_this5, extendedArea, index), 1721 | href: area.href 1722 | }); 1723 | }); 1724 | } 1725 | }, { 1726 | key: "render", 1727 | value: function render() { 1728 | var _this6 = this; 1729 | 1730 | return _react2["default"].createElement( 1731 | "div", 1732 | { style: this.styles.container, ref: function (node) { 1733 | return _this6.container = node; 1734 | } }, 1735 | _react2["default"].createElement("img", { 1736 | style: this.styles.img, 1737 | src: this.props.src, 1738 | useMap: "#" + this.state.map.name, 1739 | alt: "", 1740 | ref: function (node) { 1741 | return _this6.img = node; 1742 | }, 1743 | onLoad: this.initCanvas, 1744 | onClick: this.imageClick.bind(this), 1745 | onMouseMove: this.imageMouseMove.bind(this) 1746 | }), 1747 | _react2["default"].createElement("canvas", { ref: function (node) { 1748 | return _this6.canvas = node; 1749 | }, style: this.styles.canvas }), 1750 | _react2["default"].createElement( 1751 | "map", 1752 | { name: this.state.map.name, style: this.styles.map }, 1753 | this.renderAreas() 1754 | ) 1755 | ); 1756 | } 1757 | }]); 1758 | 1759 | return ImageMapper; 1760 | })(_react.Component); 1761 | 1762 | exports["default"] = ImageMapper; 1763 | 1764 | ImageMapper.defaultProps = { 1765 | active: true, 1766 | fillColor: "rgba(255, 255, 255, 0.5)", 1767 | lineWidth: 1, 1768 | map: { 1769 | areas: [], 1770 | name: "image-map-" + Math.random() 1771 | }, 1772 | strokeColor: "rgba(0, 0, 0, 0.5)" 1773 | }; 1774 | 1775 | ImageMapper.propTypes = { 1776 | active: _propTypes2["default"].bool, 1777 | fillColor: _propTypes2["default"].string, 1778 | height: _propTypes2["default"].number, 1779 | imgWidth: _propTypes2["default"].number, 1780 | lineWidth: _propTypes2["default"].number, 1781 | src: _propTypes2["default"].string.isRequired, 1782 | strokeColor: _propTypes2["default"].string, 1783 | width: _propTypes2["default"].number, 1784 | 1785 | onClick: _propTypes2["default"].func, 1786 | onMouseMove: _propTypes2["default"].func, 1787 | onImageClick: _propTypes2["default"].func, 1788 | onImageMouseMove: _propTypes2["default"].func, 1789 | onLoad: _propTypes2["default"].func, 1790 | onMouseEnter: _propTypes2["default"].func, 1791 | onMouseLeave: _propTypes2["default"].func, 1792 | 1793 | map: _propTypes2["default"].shape({ 1794 | areas: _propTypes2["default"].arrayOf(_propTypes2["default"].shape({ 1795 | area: _propTypes2["default"].shape({ 1796 | coords: _propTypes2["default"].arrayOf(_propTypes2["default"].number), 1797 | href: _propTypes2["default"].string, 1798 | shape: _propTypes2["default"].string, 1799 | preFillColor: _propTypes2["default"].string, 1800 | fillColor: _propTypes2["default"].string 1801 | }) 1802 | })), 1803 | name: _propTypes2["default"].string 1804 | }) 1805 | }; 1806 | module.exports = exports["default"]; 1807 | 1808 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 1809 | },{"prop-types":6,"react-fast-compare":8}]},{},[12])(12) 1810 | }); 1811 | -------------------------------------------------------------------------------- /dist/react-image-mapper.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.ImageMapper=e()}}(function(){return function e(t,r,n){function o(a,s){if(!r[a]){if(!t[a]){var c="function"==typeof require&&require;if(!s&&c)return c(a,!0);if(i)return i(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var f=r[a]={exports:{}};t[a][0].call(f.exports,function(e){var r=t[a][1][e];return o(r?r:e)},f,f.exports,e,t,r,n)}return r[a].exports}for(var i="function"==typeof require&&require,a=0;a1)for(var r=1;r1?t-1:0),n=1;n2?r-2:0),o=2;o0?n/r:1;return e.map(function(e){return e*o})}},{key:"renderPrefilledAreas",value:function(){var e=this;this.state.map.areas.map(function(t){t.preFillColor&&e["draw"+t.shape](e.scaleCoords(t.coords),t.preFillColor,t.lineWidth||e.props.lineWidth,t.strokeColor||e.props.strokeColor)})}},{key:"computeCenter",value:function(e){if(!e)return[0,0];var t=this.scaleCoords(e.coords);switch(e.shape){case"circle":return[t[0],t[1]];case"poly":case"rect":default:var r=function(){var e=t.length/2,r=t.reduce(function(t,r,n){var o=t.y,i=t.x;return n%2?{y:o+r/e,x:i}:{y:o,x:i+r/e}},{y:0,x:0}),n=r.y,o=r.x;return{v:[o,n]}}();if("object"==typeof r)return r.v}}},{key:"renderAreas",value:function(){var e=this;return this.state.map.areas.map(function(t,r){var n=e.scaleCoords(t.coords),o=e.computeCenter(t),i=u({},t,{scaledCoords:n,center:o});return d["default"].createElement("area",{key:t._id||r,shape:t.shape,coords:n.join(","),onMouseEnter:e.hoverOn.bind(e,i,r),onMouseLeave:e.hoverOff.bind(e,i,r),onMouseMove:e.mouseMove.bind(e,i,r),onClick:e.click.bind(e,i,r),href:t.href})})}},{key:"render",value:function(){var e=this;return d["default"].createElement("div",{style:this.styles.container,ref:function(t){return e.container=t}},d["default"].createElement("img",{style:this.styles.img,src:this.props.src,useMap:"#"+this.state.map.name,alt:"",ref:function(t){return e.img=t},onLoad:this.initCanvas,onClick:this.imageClick.bind(this),onMouseMove:this.imageMouseMove.bind(this)}),d["default"].createElement("canvas",{ref:function(t){return e.canvas=t},style:this.styles.canvas}),d["default"].createElement("map",{name:this.state.map.name,style:this.styles.map},this.renderAreas()))}}]),t}(p.Component);r["default"]=b,b.defaultProps={active:!0,fillColor:"rgba(255, 255, 255, 0.5)",lineWidth:1,map:{areas:[],name:"image-map-"+Math.random()},strokeColor:"rgba(0, 0, 0, 0.5)"},b.propTypes={active:h["default"].bool,fillColor:h["default"].string,height:h["default"].number,imgWidth:h["default"].number,lineWidth:h["default"].number,src:h["default"].string.isRequired,strokeColor:h["default"].string,width:h["default"].number,onClick:h["default"].func,onMouseMove:h["default"].func,onImageClick:h["default"].func,onImageMouseMove:h["default"].func,onLoad:h["default"].func,onMouseEnter:h["default"].func,onMouseLeave:h["default"].func,map:h["default"].shape({areas:h["default"].arrayOf(h["default"].shape({area:h["default"].shape({coords:h["default"].arrayOf(h["default"].number),href:h["default"].string,shape:h["default"].string,preFillColor:h["default"].string,fillColor:h["default"].string})})),name:h["default"].string})},t.exports=r["default"]}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"prop-types":6,"react-fast-compare":8}]},{},[12])(12)}); -------------------------------------------------------------------------------- /example/src/.gitignore: -------------------------------------------------------------------------------- 1 | ## This file is here to ensure it is included in the gh-pages branch, 2 | ## when `gulp deploy` is used to push updates to the demo site. 3 | 4 | # Dependency directory 5 | node_modules 6 | -------------------------------------------------------------------------------- /example/src/example.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var ReactDOM = require("react-dom"); 3 | var ImageMapper = require("react-image-mapper"); 4 | 5 | var MAP = { 6 | name: "my-map", 7 | areas: [ 8 | { 9 | name: "1", 10 | shape: "poly", 11 | coords: [25, 33, 27, 300, 128, 240, 128, 94], 12 | preFillColor: "green", 13 | fillColor: "#0000ff" 14 | }, 15 | { 16 | name: "2", 17 | shape: "poly", 18 | coords: [219, 118, 220, 210, 283, 210, 284, 119], 19 | preFillColor: "pink", 20 | lineWidth: 10, 21 | strokeColor: "#0000ff" 22 | }, 23 | { 24 | name: "3", 25 | shape: "poly", 26 | coords: [381, 241, 383, 94, 462, 53, 457, 282], 27 | preFillColor: "yellow", // this is mandatory for stroke color to work 28 | lineWidth: 10, 29 | strokeColor: "#6afd09" 30 | }, 31 | { 32 | name: "4", 33 | shape: "poly", 34 | coords: [245, 285, 290, 285, 274, 239, 249, 238], 35 | preFillColor: "red" 36 | }, 37 | { 38 | name: "5", 39 | shape: "circle", 40 | coords: [170, 100, 25], 41 | preFillColor: "rgb(255,255,255,0.3)", 42 | lineWidth: 2 43 | }, 44 | { 45 | name: "6", 46 | shape: "rect", 47 | coords: [270, 100, 200, 50], 48 | lineWidth: 2, 49 | preFillColor: "rgba(255, 255, 255, 0.3)", 50 | strokeColor: "#6afd09" 51 | } 52 | ] 53 | }; 54 | 55 | var URL = "https://c1.staticflickr.com/5/4052/4503898393_303cfbc9fd_b.jpg"; 56 | 57 | var App = React.createClass({ 58 | getInitialState() { 59 | return { hoveredArea: null, msg: null, moveMsg: null }; 60 | }, 61 | load() { 62 | this.setState({ msg: "Interact with image !" }); 63 | }, 64 | clicked(area) { 65 | this.setState({ 66 | msg: `You clicked on ${area.shape} at coords ${JSON.stringify( 67 | area.coords 68 | )} !` 69 | }); 70 | }, 71 | clickedOutside(evt) { 72 | const coords = { x: evt.nativeEvent.layerX, y: evt.nativeEvent.layerY }; 73 | this.setState({ 74 | msg: `You clicked on the image at coords ${JSON.stringify(coords)} !` 75 | }); 76 | }, 77 | moveOnImage(evt) { 78 | const coords = { x: evt.nativeEvent.layerX, y: evt.nativeEvent.layerY }; 79 | this.setState({ 80 | moveMsg: `You moved on the image at coords ${JSON.stringify(coords)} !` 81 | }); 82 | }, 83 | enterArea(area) { 84 | this.setState({ 85 | hoveredArea: area, 86 | msg: `You entered ${area.shape} ${area.name} at coords ${JSON.stringify( 87 | area.coords 88 | )} !` 89 | }); 90 | }, 91 | leaveArea(area) { 92 | this.setState({ 93 | hoveredArea: null, 94 | msg: `You leaved ${area.shape} ${area.name} at coords ${JSON.stringify( 95 | area.coords 96 | )} !` 97 | }); 98 | }, 99 | moveOnArea(area, evt) { 100 | const coords = { x: evt.nativeEvent.layerX, y: evt.nativeEvent.layerY }; 101 | this.setState({ 102 | moveMsg: `You moved on ${area.shape} ${ 103 | area.name 104 | } at coords ${JSON.stringify(coords)} !` 105 | }); 106 | }, 107 | 108 | getTipPosition(area) { 109 | return { top: `${area.center[1]}px`, left: `${area.center[0]}px` }; 110 | }, 111 | 112 | render() { 113 | return ( 114 |
115 |
116 |
117 | this.load()} 122 | onClick={area => this.clicked(area)} 123 | onMouseEnter={area => this.enterArea(area)} 124 | onMouseLeave={area => this.leaveArea(area)} 125 | onMouseMove={(area, _, evt) => this.moveOnArea(area, evt)} 126 | onImageClick={evt => this.clickedOutside(evt)} 127 | onImageMouseMove={evt => this.moveOnImage(evt)} 128 | lineWidth={4} 129 | strokeColor={"white"} 130 | /> 131 | {this.state.hoveredArea && ( 132 | 136 | {this.state.hoveredArea && this.state.hoveredArea.name} 137 | 138 | )} 139 |
140 |
141 | 						{this.state.msg ? this.state.msg : null}
142 | 					
143 |
{this.state.moveMsg ? this.state.moveMsg : null}
144 |
145 |
146 |

Example with custom tooltips :

147 |

(message logic is not present, to keep it clear)

148 |
149 | 						
150 | 							{'
\n' + 151 | " this.load()}\n" + 153 | " onClick={area => this.clicked(area)}\n" + 154 | " onMouseEnter={area => this.enterArea(area)}\n" + 155 | " onMouseLeave={area => this.leaveArea(area)}\n" + 156 | " onMouseMove={(area, _, evt) => this.moveOnArea(area, evt)}\n" + 157 | " onImageClick={evt => this.clickedOutside(evt)}\n" + 158 | " onImageMouseMove={evt => this.moveOnImage(evt)}\n" + 159 | " lineWidth={4}\n" + 160 | " strokeColor={\"white\"}\n" + 161 | " />\n" + 162 | " {\n" + 163 | " this.state.hoveredArea &&\n" + 164 | ' \n" + 166 | " { this.state.hoveredArea && this.state.hoveredArea.name}\n" + 167 | " \n" + 168 | " }\n" + 169 | "
\n"} 170 |
171 |
172 |
173 | 						
174 | 							{'URL = "https://c1.staticflickr.com/5/4052/4503898393_303cfbc9fd_b.jpg"\n' +
175 | 								"MAP = {\n" +
176 | 								'  name: "my-map",\n' +
177 | 								"  areas: [\n" +
178 | 								'    { name: "1", shape: "poly", coords: [25,33,27,300,128,240,128,94], preFillColor: "green", fillColor: "#0000ff", },\n' +
179 | 								'    { name: "2", shape: "poly", coords: [219,118,220,210,283,210,284,119], preFillColor: "pink", lineWidth: 10, strokeColor: "#0000ff" },\n' +
180 | 								'    { name: "3", shape: "poly", coords: [381,241,383,94,462,53,457,282], preFillColor: "yellow", /*this is mandatory for stroke color to work*/ lineWidth: 10, strokeColor: "#6afd09" },\n' +
181 | 								'    { name: "4", shape: "poly", coords: [245,285,290,285,274,239,249,238], preFillColor: "red"  },\n' +
182 | 								'    { name: "5", shape: "circle", coords: [170, 100, 25 ], preFillColor: "rgb(255,255,255,0.3)", lineWidth: 2 },\n' +
183 | 								'    { name: "6", shape: "rect", coords: [270, 100, 200, 50], lineWidth: 2, preFillColor: "rgba(255, 255, 255, 0.3)", strokeColor: "#6afd09" }\n' +
184 | 								"  ]\n}"}
185 | 						
186 | 					
187 | Handler details :   188 | 190 | this.setState({ codeDetails: !this.state.codeDetails }) 191 | } 192 | > 193 | {this.state.codeDetails ? "[-]" : "[+]"} 194 | 195 |
196 | 						
200 | 							{"enterArea(area) {\n" +
201 | 								"    this.setState({ hoveredArea: area });\n" +
202 | 								"}\n\n" +
203 | 								"leaveArea(area) {\n" +
204 | 								"    this.setState({ hoveredArea: null });\n" +
205 | 								"}\n\n" +
206 | 								"getTipPosition(area) {\n" +
207 | 								"    return { top: `${area.center[1]}px`, left: `${area.center[0]}px` };\n" +
208 | 								"}\n\n"}
209 | 						
210 | 					
211 | Styling details :   212 | 214 | this.setState({ stylindDetails: !this.state.stylindDetails }) 215 | } 216 | > 217 | {this.state.stylindDetails ? "[-]" : "[+]"} 218 | 219 |
220 | 						
224 | 							{".container {\n" +
225 | 								"    position: relative;\n" +
226 | 								"}\n\n" +
227 | 								".tooltip {\n" +
228 | 								"    position: absolute;\n" +
229 | 								"    color: #fff;\n" +
230 | 								"    padding: 10px;\n" +
231 | 								"    background: rgba(0,0,0,0.8);\n" +
232 | 								"    transform: translate3d(-50%, -50%, 0);\n" +
233 | 								"    border-radius: 5px;\n" +
234 | 								"    pointer-events: none;\n" +
235 | 								"    z-index: 1000;\n" +
236 | 								"}\n"}
237 | 						
238 | 					
239 |
240 |
241 | ); 242 | } 243 | }); 244 | 245 | ReactDOM.render(, document.getElementById("app")); 246 | -------------------------------------------------------------------------------- /example/src/example.less: -------------------------------------------------------------------------------- 1 | /* 2 | // Examples Stylesheet 3 | // ------------------- 4 | */ 5 | 6 | body { 7 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | color: #333; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | .tooltip { 15 | position: absolute; 16 | color: #fff; 17 | padding: 10px; 18 | background: rgba(0,0,0,0.8); 19 | transform: translate3d(-50%, -50%, 0); 20 | border-radius: 5px; 21 | pointer-events: none; 22 | z-index: 1000; 23 | } 24 | 25 | a { 26 | color: #08c; 27 | text-decoration: none; 28 | } 29 | 30 | a:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .container { 35 | margin-left: auto; 36 | margin-right: auto; 37 | padding: 1em; 38 | } 39 | 40 | .grid { 41 | margin-top: 50p; 42 | display: flex; 43 | flex-direction: row; 44 | flex-wrap: wrap; 45 | } 46 | 47 | .presenter { 48 | display: flex; 49 | flex-direction: column; 50 | align-items: center; 51 | flex-grow: 9999; 52 | } 53 | 54 | .source { 55 | margin-right: 50px; 56 | margin-left: 50px; 57 | flex-grow: 9999; 58 | } 59 | 60 | .footer { 61 | margin-top: 50px; 62 | border-top: 1px solid #eee; 63 | padding: 20px 0; 64 | font-size: 12px; 65 | color: #999; 66 | } 67 | 68 | h1, h2, h3, h4, h5, h6 { 69 | color: #222; 70 | font-weight: 100; 71 | margin: 0.5em 0; 72 | } 73 | 74 | pre.message { 75 | white-space: normal; 76 | width: 600px; 77 | text-align: center; 78 | } 79 | 80 | label { 81 | color: #999; 82 | display: inline-block; 83 | font-size: 0.85em; 84 | font-weight: bold; 85 | margin: 1em 0; 86 | text-transform: uppercase; 87 | } 88 | 89 | .hint { 90 | margin: 15px 0; 91 | font-style: italic; 92 | color: #999; 93 | } 94 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-image-mapper 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

react-image-mapper

12 |

View project on GitHub

13 |
14 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var initGulpTasks = require('react-component-gulp-tasks'); 3 | 4 | /** 5 | * Tasks are added by the react-component-gulp-tasks package 6 | * 7 | * See https://github.com/JedWatson/react-component-gulp-tasks 8 | * for documentation. 9 | * 10 | * You can also add your own additional gulp tasks if you like. 11 | */ 12 | 13 | var taskConfig = { 14 | 15 | component: { 16 | name: 'ImageMapper', 17 | dependencies: [ 18 | 'classnames', 19 | 'react', 20 | 'react-dom' 21 | ], 22 | lib: 'lib' 23 | }, 24 | 25 | example: { 26 | src: 'example/src', 27 | dist: 'example/dist', 28 | files: [ 29 | 'index.html', 30 | '.gitignore' 31 | ], 32 | scripts: [ 33 | 'example.js' 34 | ], 35 | less: [ 36 | 'example.less' 37 | ] 38 | } 39 | 40 | }; 41 | 42 | initGulpTasks(gulp, taskConfig); 43 | -------------------------------------------------------------------------------- /lib/ImageMapper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })(); 8 | 9 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 10 | 11 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 12 | 13 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 16 | 17 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 22 | 23 | var _react = require("react"); 24 | 25 | var _react2 = _interopRequireDefault(_react); 26 | 27 | var _propTypes = require("prop-types"); 28 | 29 | var _propTypes2 = _interopRequireDefault(_propTypes); 30 | 31 | var _reactFastCompare = require("react-fast-compare"); 32 | 33 | var _reactFastCompare2 = _interopRequireDefault(_reactFastCompare); 34 | 35 | var ImageMapper = (function (_Component) { 36 | _inherits(ImageMapper, _Component); 37 | 38 | function ImageMapper(props) { 39 | var _this = this; 40 | 41 | _classCallCheck(this, ImageMapper); 42 | 43 | _get(Object.getPrototypeOf(ImageMapper.prototype), "constructor", this).call(this, props); 44 | ["drawrect", "drawcircle", "drawpoly", "initCanvas", "renderPrefilledAreas"].forEach(function (f) { 45 | return _this[f] = _this[f].bind(_this); 46 | }); 47 | var absPos = { position: "absolute", top: 0, left: 0 }; 48 | this.styles = { 49 | container: { position: "relative" }, 50 | canvas: _extends({}, absPos, { pointerEvents: "none", zIndex: 2 }), 51 | img: _extends({}, absPos, { zIndex: 1, userSelect: "none" }), 52 | map: props.onClick && { cursor: "pointer" } || undefined 53 | }; 54 | // Props watched for changes to trigger update 55 | this.watchedProps = ["active", "fillColor", "height", "imgWidth", "lineWidth", "src", "strokeColor", "width"]; 56 | } 57 | 58 | _createClass(ImageMapper, [{ 59 | key: "shouldComponentUpdate", 60 | value: function shouldComponentUpdate(nextProps) { 61 | var _this2 = this; 62 | 63 | var propChanged = this.watchedProps.some(function (prop) { 64 | return _this2.props[prop] !== nextProps[prop]; 65 | }); 66 | return !(0, _reactFastCompare2["default"])(this.props.map, this.state.map) || propChanged; 67 | } 68 | }, { 69 | key: "componentWillMount", 70 | value: function componentWillMount() { 71 | this.updateCacheMap(); 72 | } 73 | }, { 74 | key: "updateCacheMap", 75 | value: function updateCacheMap() { 76 | this.setState({ map: JSON.parse(JSON.stringify(this.props.map)) }, this.initCanvas); 77 | } 78 | }, { 79 | key: "componentDidUpdate", 80 | value: function componentDidUpdate() { 81 | this.updateCacheMap(); 82 | this.initCanvas(); 83 | } 84 | }, { 85 | key: "drawrect", 86 | value: function drawrect(coords, fillColor, lineWidth, strokeColor) { 87 | var _coords = _slicedToArray(coords, 4); 88 | 89 | var left = _coords[0]; 90 | var top = _coords[1]; 91 | var right = _coords[2]; 92 | var bot = _coords[3]; 93 | 94 | this.ctx.fillStyle = fillColor; 95 | this.ctx.lineWidth = lineWidth; 96 | this.ctx.strokeStyle = strokeColor; 97 | this.ctx.strokeRect(left, top, right - left, bot - top); 98 | this.ctx.fillRect(left, top, right - left, bot - top); 99 | this.ctx.fillStyle = this.props.fillColor; 100 | } 101 | }, { 102 | key: "drawcircle", 103 | value: function drawcircle(coords, fillColor, lineWidth, strokeColor) { 104 | this.ctx.fillStyle = fillColor; 105 | this.ctx.beginPath(); 106 | this.ctx.lineWidth = lineWidth; 107 | this.ctx.strokeStyle = strokeColor; 108 | this.ctx.arc(coords[0], coords[1], coords[2], 0, 2 * Math.PI); 109 | this.ctx.closePath(); 110 | this.ctx.stroke(); 111 | this.ctx.fill(); 112 | this.ctx.fillStyle = this.props.fillColor; 113 | } 114 | }, { 115 | key: "drawpoly", 116 | value: function drawpoly(coords, fillColor, lineWidth, strokeColor) { 117 | var _this3 = this; 118 | 119 | coords = coords.reduce(function (a, v, i, s) { 120 | return i % 2 ? a : [].concat(_toConsumableArray(a), [s.slice(i, i + 2)]); 121 | }, []); 122 | 123 | this.ctx.fillStyle = fillColor; 124 | this.ctx.beginPath(); 125 | this.ctx.lineWidth = lineWidth; 126 | this.ctx.strokeStyle = strokeColor; 127 | var first = coords.unshift(); 128 | this.ctx.moveTo(first[0], first[1]); 129 | coords.forEach(function (c) { 130 | return _this3.ctx.lineTo(c[0], c[1]); 131 | }); 132 | this.ctx.closePath(); 133 | this.ctx.stroke(); 134 | this.ctx.fill(); 135 | this.ctx.fillStyle = this.props.fillColor; 136 | } 137 | }, { 138 | key: "initCanvas", 139 | value: function initCanvas() { 140 | if (this.props.width) this.img.width = this.props.width; 141 | 142 | if (this.props.height) this.img.height = this.props.height; 143 | 144 | this.canvas.width = this.props.width || this.img.clientWidth; 145 | this.canvas.height = this.props.height || this.img.clientHeight; 146 | this.container.style.width = (this.props.width || this.img.clientWidth) + "px"; 147 | this.container.style.height = (this.props.height || this.img.clientHeight) + "px"; 148 | this.ctx = this.canvas.getContext("2d"); 149 | this.ctx.fillStyle = this.props.fillColor; 150 | //this.ctx.strokeStyle = this.props.strokeColor; 151 | 152 | if (this.props.onLoad) this.props.onLoad(); 153 | 154 | this.renderPrefilledAreas(); 155 | } 156 | }, { 157 | key: "hoverOn", 158 | value: function hoverOn(area, index, event) { 159 | var shape = event.target.getAttribute("shape"); 160 | 161 | if (this.props.active && this["draw" + shape]) { 162 | this["draw" + shape](event.target.getAttribute("coords").split(","), area.fillColor, area.lineWidth || this.props.lineWidth, area.strokeColor || this.props.strokeColor); 163 | } 164 | if (this.props.onMouseEnter) this.props.onMouseEnter(area, index, event); 165 | } 166 | }, { 167 | key: "hoverOff", 168 | value: function hoverOff(area, index, event) { 169 | if (this.props.active) { 170 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 171 | this.renderPrefilledAreas(); 172 | } 173 | 174 | if (this.props.onMouseLeave) this.props.onMouseLeave(area, index, event); 175 | } 176 | }, { 177 | key: "click", 178 | value: function click(area, index, event) { 179 | if (this.props.onClick) { 180 | event.preventDefault(); 181 | this.props.onClick(area, index, event); 182 | } 183 | } 184 | }, { 185 | key: "imageClick", 186 | value: function imageClick(event) { 187 | if (this.props.onImageClick) { 188 | event.preventDefault(); 189 | this.props.onImageClick(event); 190 | } 191 | } 192 | }, { 193 | key: "mouseMove", 194 | value: function mouseMove(area, index, event) { 195 | if (this.props.onMouseMove) { 196 | this.props.onMouseMove(area, index, event); 197 | } 198 | } 199 | }, { 200 | key: "imageMouseMove", 201 | value: function imageMouseMove(area, index, event) { 202 | if (this.props.onImageMouseMove) { 203 | this.props.onImageMouseMove(area, index, event); 204 | } 205 | } 206 | }, { 207 | key: "scaleCoords", 208 | value: function scaleCoords(coords) { 209 | var _props = this.props; 210 | var imgWidth = _props.imgWidth; 211 | var width = _props.width; 212 | 213 | // calculate scale based on current 'width' and the original 'imgWidth' 214 | var scale = width && imgWidth && imgWidth > 0 ? width / imgWidth : 1; 215 | return coords.map(function (coord) { 216 | return coord * scale; 217 | }); 218 | } 219 | }, { 220 | key: "renderPrefilledAreas", 221 | value: function renderPrefilledAreas() { 222 | var _this4 = this; 223 | 224 | this.state.map.areas.map(function (area) { 225 | if (!area.preFillColor) return; 226 | _this4["draw" + area.shape](_this4.scaleCoords(area.coords), area.preFillColor, area.lineWidth || _this4.props.lineWidth, area.strokeColor || _this4.props.strokeColor); 227 | }); 228 | } 229 | }, { 230 | key: "computeCenter", 231 | value: function computeCenter(area) { 232 | if (!area) return [0, 0]; 233 | 234 | var scaledCoords = this.scaleCoords(area.coords); 235 | 236 | switch (area.shape) { 237 | case "circle": 238 | return [scaledCoords[0], scaledCoords[1]]; 239 | case "poly": 240 | case "rect": 241 | default: 242 | { 243 | var _ret = (function () { 244 | // Calculate centroid 245 | var n = scaledCoords.length / 2; 246 | 247 | var _scaledCoords$reduce = scaledCoords.reduce(function (_ref, val, idx) { 248 | var y = _ref.y; 249 | var x = _ref.x; 250 | 251 | return !(idx % 2) ? { y: y, x: x + val / n } : { y: y + val / n, x: x }; 252 | }, { y: 0, x: 0 }); 253 | 254 | var y = _scaledCoords$reduce.y; 255 | var x = _scaledCoords$reduce.x; 256 | 257 | return { 258 | v: [x, y] 259 | }; 260 | })(); 261 | 262 | if (typeof _ret === "object") return _ret.v; 263 | } 264 | } 265 | } 266 | }, { 267 | key: "renderAreas", 268 | value: function renderAreas() { 269 | var _this5 = this; 270 | 271 | return this.state.map.areas.map(function (area, index) { 272 | var scaledCoords = _this5.scaleCoords(area.coords); 273 | var center = _this5.computeCenter(area); 274 | var extendedArea = _extends({}, area, { scaledCoords: scaledCoords, center: center }); 275 | return _react2["default"].createElement("area", { 276 | key: area._id || index, 277 | shape: area.shape, 278 | coords: scaledCoords.join(","), 279 | onMouseEnter: _this5.hoverOn.bind(_this5, extendedArea, index), 280 | onMouseLeave: _this5.hoverOff.bind(_this5, extendedArea, index), 281 | onMouseMove: _this5.mouseMove.bind(_this5, extendedArea, index), 282 | onClick: _this5.click.bind(_this5, extendedArea, index), 283 | href: area.href 284 | }); 285 | }); 286 | } 287 | }, { 288 | key: "render", 289 | value: function render() { 290 | var _this6 = this; 291 | 292 | return _react2["default"].createElement( 293 | "div", 294 | { style: this.styles.container, ref: function (node) { 295 | return _this6.container = node; 296 | } }, 297 | _react2["default"].createElement("img", { 298 | style: this.styles.img, 299 | src: this.props.src, 300 | useMap: "#" + this.state.map.name, 301 | alt: "", 302 | ref: function (node) { 303 | return _this6.img = node; 304 | }, 305 | onLoad: this.initCanvas, 306 | onClick: this.imageClick.bind(this), 307 | onMouseMove: this.imageMouseMove.bind(this) 308 | }), 309 | _react2["default"].createElement("canvas", { ref: function (node) { 310 | return _this6.canvas = node; 311 | }, style: this.styles.canvas }), 312 | _react2["default"].createElement( 313 | "map", 314 | { name: this.state.map.name, style: this.styles.map }, 315 | this.renderAreas() 316 | ) 317 | ); 318 | } 319 | }]); 320 | 321 | return ImageMapper; 322 | })(_react.Component); 323 | 324 | exports["default"] = ImageMapper; 325 | 326 | ImageMapper.defaultProps = { 327 | active: true, 328 | fillColor: "rgba(255, 255, 255, 0.5)", 329 | lineWidth: 1, 330 | map: { 331 | areas: [], 332 | name: "image-map-" + Math.random() 333 | }, 334 | strokeColor: "rgba(0, 0, 0, 0.5)" 335 | }; 336 | 337 | ImageMapper.propTypes = { 338 | active: _propTypes2["default"].bool, 339 | fillColor: _propTypes2["default"].string, 340 | height: _propTypes2["default"].number, 341 | imgWidth: _propTypes2["default"].number, 342 | lineWidth: _propTypes2["default"].number, 343 | src: _propTypes2["default"].string.isRequired, 344 | strokeColor: _propTypes2["default"].string, 345 | width: _propTypes2["default"].number, 346 | 347 | onClick: _propTypes2["default"].func, 348 | onMouseMove: _propTypes2["default"].func, 349 | onImageClick: _propTypes2["default"].func, 350 | onImageMouseMove: _propTypes2["default"].func, 351 | onLoad: _propTypes2["default"].func, 352 | onMouseEnter: _propTypes2["default"].func, 353 | onMouseLeave: _propTypes2["default"].func, 354 | 355 | map: _propTypes2["default"].shape({ 356 | areas: _propTypes2["default"].arrayOf(_propTypes2["default"].shape({ 357 | area: _propTypes2["default"].shape({ 358 | coords: _propTypes2["default"].arrayOf(_propTypes2["default"].number), 359 | href: _propTypes2["default"].string, 360 | shape: _propTypes2["default"].string, 361 | preFillColor: _propTypes2["default"].string, 362 | fillColor: _propTypes2["default"].string 363 | }) 364 | })), 365 | name: _propTypes2["default"].string 366 | }) 367 | }; 368 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-image-mapper", 3 | "version": "0.0.15", 4 | "description": "imageMapper", 5 | "main": "lib/ImageMapper.js", 6 | "author": "Coldiary", 7 | "license": "MIT", 8 | "homepage": "https://github.com/coldiary/react-image-mapper", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/coldiary/react-image-mapper.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/coldiary/react-image-mapper/issues" 15 | }, 16 | "dependencies": { 17 | "classnames": "^2.1.2", 18 | "prop-types": "^15.6.0", 19 | "react-fast-compare": "^2.0.4", 20 | "require-dir": "1.2.0" 21 | }, 22 | "devDependencies": { 23 | "babel-eslint": "^4.1.3", 24 | "babel-register": "^6.26.0", 25 | "chai": "^4.0.0", 26 | "enzyme": "^3.0.0", 27 | "enzyme-adapter-react-14": "^1.0.5", 28 | "eslint": "^1.6.0", 29 | "eslint-plugin-react": "^3.5.1", 30 | "gulp": "^3.9.1", 31 | "jsdom": "^11.6.2", 32 | "jsdom-global": "^3.0.2", 33 | "mocha": "^5.0.4", 34 | "react": "^0.14.0", 35 | "react-addons-test-utils": "^0.14.8", 36 | "react-component-gulp-tasks": "^0.7.6", 37 | "react-dom": "^0.14.0" 38 | }, 39 | "peerDependencies": { 40 | "react": "^0.14.0" 41 | }, 42 | "browserify-shim": { 43 | "react": "global:React" 44 | }, 45 | "scripts": { 46 | "build": "gulp clean && NODE_ENV=production gulp build", 47 | "examples": "gulp dev:server", 48 | "lint": "eslint ./; true", 49 | "publish:site": "NODE_ENV=production gulp publish:examples", 50 | "release": "NODE_ENV=production gulp release", 51 | "start": "gulp dev", 52 | "test": "mocha --require babel-core/register --require jsdom-global/register ./test/**/*.test.js", 53 | "test:watch": "npm test -- --watch", 54 | "watch": "gulp watch:lib" 55 | }, 56 | "keywords": [ 57 | "react", 58 | "react-component" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /src/ImageMapper.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import isEqual from "react-fast-compare"; 4 | 5 | export default class ImageMapper extends Component { 6 | constructor(props) { 7 | super(props); 8 | [ 9 | "drawrect", 10 | "drawcircle", 11 | "drawpoly", 12 | "initCanvas", 13 | "renderPrefilledAreas" 14 | ].forEach(f => (this[f] = this[f].bind(this))); 15 | let absPos = { position: "absolute", top: 0, left: 0 }; 16 | this.styles = { 17 | container: { position: "relative" }, 18 | canvas: { ...absPos, pointerEvents: "none", zIndex: 2 }, 19 | img: { ...absPos, zIndex: 1, userSelect: "none" }, 20 | map: (props.onClick && { cursor: "pointer" }) || undefined 21 | }; 22 | // Props watched for changes to trigger update 23 | this.watchedProps = [ 24 | "active", 25 | "fillColor", 26 | "height", 27 | "imgWidth", 28 | "lineWidth", 29 | "src", 30 | "strokeColor", 31 | "width" 32 | ]; 33 | } 34 | 35 | shouldComponentUpdate(nextProps) { 36 | const propChanged = this.watchedProps.some( 37 | prop => this.props[prop] !== nextProps[prop] 38 | ); 39 | return !isEqual(this.props.map, this.state.map) || propChanged; 40 | } 41 | 42 | componentWillMount() { 43 | this.updateCacheMap(); 44 | } 45 | 46 | updateCacheMap() { 47 | this.setState( 48 | { map: JSON.parse(JSON.stringify(this.props.map)) }, 49 | this.initCanvas 50 | ); 51 | } 52 | 53 | componentDidUpdate() { 54 | this.updateCacheMap(); 55 | this.initCanvas(); 56 | } 57 | 58 | drawrect(coords, fillColor, lineWidth, strokeColor) { 59 | let [left, top, right, bot] = coords; 60 | this.ctx.fillStyle = fillColor; 61 | this.ctx.lineWidth = lineWidth; 62 | this.ctx.strokeStyle = strokeColor; 63 | this.ctx.strokeRect(left, top, right - left, bot - top); 64 | this.ctx.fillRect(left, top, right - left, bot - top); 65 | this.ctx.fillStyle = this.props.fillColor; 66 | } 67 | 68 | drawcircle(coords, fillColor, lineWidth, strokeColor) { 69 | this.ctx.fillStyle = fillColor; 70 | this.ctx.beginPath(); 71 | this.ctx.lineWidth = lineWidth; 72 | this.ctx.strokeStyle = strokeColor; 73 | this.ctx.arc(coords[0], coords[1], coords[2], 0, 2 * Math.PI); 74 | this.ctx.closePath(); 75 | this.ctx.stroke(); 76 | this.ctx.fill(); 77 | this.ctx.fillStyle = this.props.fillColor; 78 | } 79 | 80 | drawpoly(coords, fillColor, lineWidth, strokeColor) { 81 | coords = coords.reduce( 82 | (a, v, i, s) => (i % 2 ? a : [...a, s.slice(i, i + 2)]), 83 | [] 84 | ); 85 | 86 | this.ctx.fillStyle = fillColor; 87 | this.ctx.beginPath(); 88 | this.ctx.lineWidth = lineWidth; 89 | this.ctx.strokeStyle = strokeColor; 90 | let first = coords.unshift(); 91 | this.ctx.moveTo(first[0], first[1]); 92 | coords.forEach(c => this.ctx.lineTo(c[0], c[1])); 93 | this.ctx.closePath(); 94 | this.ctx.stroke(); 95 | this.ctx.fill(); 96 | this.ctx.fillStyle = this.props.fillColor; 97 | } 98 | 99 | initCanvas() { 100 | if (this.props.width) this.img.width = this.props.width; 101 | 102 | if (this.props.height) this.img.height = this.props.height; 103 | 104 | this.canvas.width = this.props.width || this.img.clientWidth; 105 | this.canvas.height = this.props.height || this.img.clientHeight; 106 | this.container.style.width = 107 | (this.props.width || this.img.clientWidth) + "px"; 108 | this.container.style.height = 109 | (this.props.height || this.img.clientHeight) + "px"; 110 | this.ctx = this.canvas.getContext("2d"); 111 | this.ctx.fillStyle = this.props.fillColor; 112 | //this.ctx.strokeStyle = this.props.strokeColor; 113 | 114 | if (this.props.onLoad) this.props.onLoad(); 115 | 116 | this.renderPrefilledAreas(); 117 | } 118 | 119 | hoverOn(area, index, event) { 120 | const shape = event.target.getAttribute("shape"); 121 | 122 | if (this.props.active && this["draw" + shape]) { 123 | this["draw" + shape]( 124 | event.target.getAttribute("coords").split(","), 125 | area.fillColor, 126 | area.lineWidth || this.props.lineWidth, 127 | area.strokeColor || this.props.strokeColor 128 | ); 129 | } 130 | if (this.props.onMouseEnter) this.props.onMouseEnter(area, index, event); 131 | } 132 | 133 | hoverOff(area, index, event) { 134 | if (this.props.active) { 135 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 136 | this.renderPrefilledAreas(); 137 | } 138 | 139 | if (this.props.onMouseLeave) this.props.onMouseLeave(area, index, event); 140 | } 141 | 142 | click(area, index, event) { 143 | if (this.props.onClick) { 144 | event.preventDefault(); 145 | this.props.onClick(area, index, event); 146 | } 147 | } 148 | 149 | imageClick(event) { 150 | if (this.props.onImageClick) { 151 | event.preventDefault(); 152 | this.props.onImageClick(event); 153 | } 154 | } 155 | 156 | mouseMove(area, index, event) { 157 | if (this.props.onMouseMove) { 158 | this.props.onMouseMove(area, index, event); 159 | } 160 | } 161 | 162 | imageMouseMove(area, index, event) { 163 | if (this.props.onImageMouseMove) { 164 | this.props.onImageMouseMove(area, index, event); 165 | } 166 | } 167 | 168 | scaleCoords(coords) { 169 | const { imgWidth, width } = this.props; 170 | // calculate scale based on current 'width' and the original 'imgWidth' 171 | const scale = width && imgWidth && imgWidth > 0 ? width / imgWidth : 1; 172 | return coords.map(coord => coord * scale); 173 | } 174 | 175 | renderPrefilledAreas() { 176 | this.state.map.areas.map(area => { 177 | if (!area.preFillColor) return; 178 | this["draw" + area.shape]( 179 | this.scaleCoords(area.coords), 180 | area.preFillColor, 181 | area.lineWidth || this.props.lineWidth, 182 | area.strokeColor || this.props.strokeColor 183 | ); 184 | }); 185 | } 186 | 187 | computeCenter(area) { 188 | if (!area) return [0, 0]; 189 | 190 | const scaledCoords = this.scaleCoords(area.coords); 191 | 192 | switch (area.shape) { 193 | case "circle": 194 | return [scaledCoords[0], scaledCoords[1]]; 195 | case "poly": 196 | case "rect": 197 | default: { 198 | // Calculate centroid 199 | const n = scaledCoords.length / 2; 200 | const { y, x } = scaledCoords.reduce( 201 | ({ y, x }, val, idx) => { 202 | return !(idx % 2) ? { y, x: x + val / n } : { y: y + val / n, x }; 203 | }, 204 | { y: 0, x: 0 } 205 | ); 206 | return [x, y]; 207 | } 208 | } 209 | } 210 | 211 | renderAreas() { 212 | return this.state.map.areas.map((area, index) => { 213 | const scaledCoords = this.scaleCoords(area.coords); 214 | const center = this.computeCenter(area); 215 | const extendedArea = { ...area, scaledCoords, center }; 216 | return ( 217 | 227 | ); 228 | }); 229 | } 230 | 231 | render() { 232 | return ( 233 |
(this.container = node)}> 234 | (this.img = node)} 240 | onLoad={this.initCanvas} 241 | onClick={this.imageClick.bind(this)} 242 | onMouseMove={this.imageMouseMove.bind(this)} 243 | /> 244 | (this.canvas = node)} style={this.styles.canvas} /> 245 | 246 | {this.renderAreas()} 247 | 248 |
249 | ); 250 | } 251 | } 252 | 253 | ImageMapper.defaultProps = { 254 | active: true, 255 | fillColor: "rgba(255, 255, 255, 0.5)", 256 | lineWidth: 1, 257 | map: { 258 | areas: [], 259 | name: "image-map-" + Math.random() 260 | }, 261 | strokeColor: "rgba(0, 0, 0, 0.5)" 262 | }; 263 | 264 | ImageMapper.propTypes = { 265 | active: PropTypes.bool, 266 | fillColor: PropTypes.string, 267 | height: PropTypes.number, 268 | imgWidth: PropTypes.number, 269 | lineWidth: PropTypes.number, 270 | src: PropTypes.string.isRequired, 271 | strokeColor: PropTypes.string, 272 | width: PropTypes.number, 273 | 274 | onClick: PropTypes.func, 275 | onMouseMove: PropTypes.func, 276 | onImageClick: PropTypes.func, 277 | onImageMouseMove: PropTypes.func, 278 | onLoad: PropTypes.func, 279 | onMouseEnter: PropTypes.func, 280 | onMouseLeave: PropTypes.func, 281 | 282 | map: PropTypes.shape({ 283 | areas: PropTypes.arrayOf( 284 | PropTypes.shape({ 285 | area: PropTypes.shape({ 286 | coords: PropTypes.arrayOf(PropTypes.number), 287 | href: PropTypes.string, 288 | shape: PropTypes.string, 289 | preFillColor: PropTypes.string, 290 | fillColor: PropTypes.string 291 | }) 292 | }) 293 | ), 294 | name: PropTypes.string 295 | }) 296 | }; 297 | -------------------------------------------------------------------------------- /test/ImageMapper.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { configure, mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-14'; 5 | import ImageMapper from '../src/ImageMapper'; 6 | 7 | configure({ adapter: new Adapter() }); 8 | 9 | const URL = 'https://c1.staticflickr.com/5/4052/4503898393_303cfbc9fd_b.jpg'; 10 | const MAP = { 11 | name: 'my-map', 12 | areas: [ 13 | { shape: 'poly', coords: [25,33,27,300,128,240,128,94] }, 14 | { shape: 'poly', coords: [219,118,220,210,283,210,284,119] }, 15 | { shape: 'poly', coords: [381,241,383,94,462,53,457,282] }, 16 | { shape: 'poly', coords: [245,285,290,285,274,239,249,238] }, 17 | ] 18 | }; 19 | 20 | const render = (props) => { 21 | const wrapper = mount(); 22 | const instance = wrapper.instance(); 23 | instance.canvas = { getContext: () => ({}) }; 24 | instance.initCanvas(); 25 | 26 | return instance; 27 | }; 28 | 29 | describe('ImageMapper', () => { 30 | describe('when width prop is provided', () => { 31 | it('container width should be equal to width prop', () => { 32 | const instance = render({ width: 100 }); 33 | const containerStyles = instance.container.style; 34 | 35 | expect(containerStyles).to.have.property('width'); 36 | expect(containerStyles.width).to.equal('100px'); 37 | }); 38 | 39 | it('canvas width should be equal to width prop', () => { 40 | const instance = render({ width: 100 }); 41 | const canvas = instance.canvas; 42 | 43 | expect(canvas).to.have.property('width'); 44 | expect(canvas.width).to.equal(100); 45 | }); 46 | 47 | it('image width should be equal to width prop', () => { 48 | const instance = render({ width: 100 }); 49 | const img = instance.img; 50 | 51 | expect(img).to.have.property('width'); 52 | expect(img.width).to.equal(100); 53 | }); 54 | }); 55 | 56 | describe('when height prop is provided', () => { 57 | it('container height should be equal to height prop', () => { 58 | const instance = render({ height: 100 }); 59 | const containerStyles = instance.container.style; 60 | 61 | expect(containerStyles).to.have.property('height'); 62 | expect(containerStyles.height).to.equal('100px'); 63 | }); 64 | 65 | it('canvas height should be equal to height prop', () => { 66 | const instance = render({ height: 100 }); 67 | const canvas = instance.canvas; 68 | 69 | expect(canvas).to.have.property('height'); 70 | expect(canvas.height).to.equal(100); 71 | }); 72 | 73 | it('img height should be equal to height prop', () => { 74 | const instance = render({ height: 100 }); 75 | const img = instance.img; 76 | 77 | expect(img).to.have.property('height'); 78 | expect(img.height).to.equal(100); 79 | }); 80 | }); 81 | 82 | describe('when onClick prop is provided', () => { 83 | it('map styles should have "cursor:pointer"', () => { 84 | const wrapper = mount( {}} />); 85 | const mapStyles = wrapper.find('map').get(0).props.style; 86 | 87 | expect(mapStyles).to.have.property('cursor'); 88 | expect(mapStyles.cursor).to.equal('pointer'); 89 | }); 90 | }); 91 | }); 92 | --------------------------------------------------------------------------------