├── .babelrc ├── .gitignore ├── LICENCE ├── README.md ├── build ├── asset-manifest.json ├── favicon.ico ├── manifest.json ├── service-worker.js └── static │ ├── css │ ├── main.f083a81c.css │ └── main.f083a81c.css.map │ └── js │ ├── main.1acfb940.js │ └── main.1acfb940.js.map ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── dist ├── react-minimap.css └── react-minimap.js ├── example ├── App.css ├── App.js ├── App.test.js ├── Block.js ├── index.css ├── index.js └── registerServiceWorker.js ├── index.html ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── __tests__ │ └── Minimap.spec.js ├── components │ └── Child.js ├── index.js ├── react-minimap.css └── react-minimap.js ├── webpack.production.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | 10 | # misc 11 | .DS_Store 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | 22 | # Build 23 | example/build/app.js 24 | 25 | # Logs 26 | logs 27 | *.log 28 | 29 | # Dependency directory 30 | node_modules 31 | 32 | .idea -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jérémy CARBONNE 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-minimap 2 | 3 | [![NPM](https://img.shields.io/npm/v/react-minimap.svg?style=flat-square)](https://www.npmjs.com/package/react-minimap) 4 | 5 | A minimap for React based on [jquery-minimap](https://github.com/john-bai/jquery-minimap) 6 | 7 | ## Demo 8 | 9 | [react-minimap](https://jeremy-carbonne.github.io/react-minimap/) 10 | 11 | ## Installation 12 | 13 | `npm i --save react-minimap` 14 | 15 | ## Usage 16 | ```js 17 | import Minimap from 'react-minimap'; 18 | import 'react-minimap/dist/react-minimap.css'; 19 | ``` 20 | 21 | ```html 22 | 23 |
24 |

Title

25 |
26 |
27 |

Title 2

28 |
29 |

Titles are never rendered by the Minimap

30 |
31 |
32 |
33 | ``` 34 | 35 | ## Configuration 36 | 37 | The `Minimap` supports the following props: 38 | 39 | | Prop name | Type | Default value | Description | 40 | |------------------|---------------------------------|----------------------------|------------------------------------------------------------------------------------------| 41 | | selector | string | is required | A css selector for specify what you want to render inside the minimap | 42 | | className | string | '' | A className for the minimap component | 43 | | width | number | `200` | | 44 | | height | number | `200` | | 45 | | keepAspectRatio | boolean | `false` | | 46 | | childComponent | any | Internal Component | Allows customizing how components matched by selector are rendered (optional) | 47 | | onMountCenterOnX | boolean | `false` | | 48 | | onMountCenterOnY | boolean | `false` | | 49 | 50 | -------------------------------------------------------------------------------- /build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "static/css/main.f083a81c.css", 3 | "main.css.map": "static/css/main.f083a81c.css.map", 4 | "main.js": "static/js/main.1acfb940.js", 5 | "main.js.map": "static/js/main.1acfb940.js.map" 6 | } -------------------------------------------------------------------------------- /build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremy-carbonne/react-minimap/22cd3da355187ff3269498c11d517d8947d60e5f/build/favicon.ico -------------------------------------------------------------------------------- /build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /build/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}var precacheConfig=[["/Users/jeremy/react-minimap/index.html","82f58107ab693606c9c4b2ff5f1e9114"],["build/static/css/main.f083a81c.css","8213347da2ffcd7bd757df0bc180bd7d"],["build/static/js/main.1acfb940.js","505db242c8b3dbade4acadacd17a2663"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.hash="",n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var r=new Request(n,{credentials:"same-origin"});return fetch(r).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);(t=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],e.request.url)&&(n=new URL("build/index.html",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}); -------------------------------------------------------------------------------- /build/static/css/main.f083a81c.css: -------------------------------------------------------------------------------- 1 | body{margin:0;padding:0;font-family:sans-serif}.App{text-align:center}.nav-bar{height:60px;background-color:#000;color:#fff;line-height:2.7em;font-size:1.2em}.container{height:calc(100vh - 60px);position:absolute;width:100vw;background-color:#f5f5f5}.pos-rlt{position:relative}.box{width:200px;padding:10px 0}.yellow{background-color:#ffee58}.red{background-color:#f44336}.blue{background-color:blue}.minimap{margin:30px}.card-container{color:#d4cd96;font-family:Fira Sans,sans-serif;-webkit-transform-origin:100% 50%;-ms-transform-origin:100% 50%;transform-origin:100% 50%;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-tap-highlight-color:transparent;-webkit-transition:-webkit-transform .6s cubic-bezier(.49,.23,.58,.49);transition:-webkit-transform .6s cubic-bezier(.49,.23,.58,.49);-o-transition:transform .6s cubic-bezier(.49,.23,.58,.49);transition:transform .6s cubic-bezier(.49,.23,.58,.49);transition:transform .6s cubic-bezier(.49,.23,.58,.49),-webkit-transform .6s cubic-bezier(.49,.23,.58,.49)}.dark{-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-backface-visibility:hidden;backface-visibility:hidden;background:-webkit-repeating-linear-gradient(315deg,rgba(0,0,0,.3),transparent 1px,rgba(0,0,0,.3) 2px);background:-o-repeating-linear-gradient(315deg,rgba(0,0,0,.3),transparent 1px,rgba(0,0,0,.3) 2px);background:repeating-linear-gradient(135deg,rgba(0,0,0,.3),transparent 1px,rgba(0,0,0,.3) 2px);background-size:3px 3px;background-color:#302f34;border-radius:1px;-webkit-box-shadow:0 -6px 8px 0 rgba(0,0,0,.1),0 6px 8px 0 rgba(0,0,0,.1),6px 0 8px 0 rgba(0,0,0,.1),-6px 0 8px 0 rgba(0,0,0,.1);box-shadow:0 -6px 8px 0 rgba(0,0,0,.1),0 6px 8px 0 rgba(0,0,0,.1),6px 0 8px 0 rgba(0,0,0,.1),-6px 0 8px 0 rgba(0,0,0,.1);cursor:pointer;padding:5%}.commit-hash{position:absolute;right:5px;bottom:5px;color:hsla(0,0%,77%,.66)}.keep-aspect-ratio{position:absolute;top:2px;right:7px;height:60px;padding-top:1px;font-size:.8em}a{color:#fff}a:active,a:hover,a:link,a:visited{text-decoration:none}.minimap-viewport{position:absolute;-webkit-box-sizing:border-box;box-sizing:border-box;background-color:rgba(0,0,0,.4);z-index:1;cursor:move}.minimap{float:right;position:fixed;right:0;z-index:1;margin:10px;background-color:rgba(0,0,0,.29);border:1px solid rgba(0,0,0,.17)}.minimap-container{overflow:scroll;width:100%;height:100%}.minimap-children{background:#ccc;border:1px solid #000;-webkit-box-sizing:border-box;box-sizing:border-box} 2 | /*# sourceMappingURL=main.f083a81c.css.map*/ -------------------------------------------------------------------------------- /build/static/css/main.f083a81c.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["index.css","App.css","../dist/react-minimap.css"],"names":[],"mappings":"AAAA,KACE,SACA,UACA,sBAAwB,CCH1B,KACC,iBAAmB,CAGpB,SACC,YACA,sBACA,WACA,kBACA,eAAiB,CAGlB,WACC,0BACA,kBACA,YACA,wBAA0B,CAG3B,SACC,iBAAmB,CAGpB,KACC,YACA,cAAmB,CAEpB,QAAU,wBAA0B,CACpC,KAAO,wBAA0B,CACjC,MAAQ,qBAAuB,CAE/B,SACC,WAAY,CAGb,gBACC,cACA,iCACA,kCACI,8BACA,0BACJ,oCACI,4BACJ,wCACA,uEACA,+DACA,0DACA,uDACA,0GAA6H,CAG9H,MACC,8BACQ,sBACR,mCACI,2BACJ,uGACA,kGACA,+FACA,wBACA,yBACA,kBACA,iIACQ,yHACR,eACA,UAAY,CAGb,aACC,kBACA,UACA,WACA,wBAAiC,CAGlC,mBACC,kBACA,QACA,UACA,YACA,gBACA,cAAiB,CAGlB,EACC,UAAY,CAGb,kCAEE,oBAAsB,CC1FxB,kBAAkB,kBAAkB,8BAA8B,sBAAsB,gCAAgC,UAAU,WAAW,CAAC,SAAS,YAAY,eAAe,QAAQ,UAAU,YAAY,iCAAiC,gCAAgC,CAAC,mBAAmB,gBAAgB,WAAW,WAAW,CAAC,kBAAkB,gBAAgB,sBAAsB,8BAA8B,qBAAqB","file":"static/css/main.f083a81c.css","sourcesContent":["body {\n margin: 0;\n padding: 0;\n font-family: sans-serif;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./example/index.css",".App {\n\ttext-align: center;\n}\n\n.nav-bar {\n\theight: 60px;\n\tbackground-color: black; \n\tcolor: white; \n\tline-height: 2.7em;\n\tfont-size: 1.2em;\n}\n\n.container {\n\theight: calc(100vh - 60px); \n\tposition: absolute;\n\twidth: 100vw; \n\tbackground-color: #f5f5f5;\n}\n\n.pos-rlt{\n\tposition: relative;\n}\n\n.box {\n\twidth: 200px;\n\tpadding: 10px 0px ;\n}\n.yellow { background-color: #FFEE58; }\n.red { background-color: #f44336; }\n.blue { background-color: blue; }\n\n.minimap {\n\tmargin: 30px\n}\n\n.card-container {\n\tcolor: #d4cd96;\n\tfont-family: 'Fira Sans', sans-serif;\n\t-webkit-transform-origin: 100% 50%;\n\t\t\t\t\t-ms-transform-origin: 100% 50%;\n\t transform-origin: 100% 50%;\n\t-webkit-transform-style: preserve-3d;\n\t\t\t\t\ttransform-style: preserve-3d;\n\t-webkit-tap-highlight-color: transparent;\n\t-webkit-transition: -webkit-transform 0.6s cubic-bezier(0.49, 0.23, 0.58, 0.49);\n\ttransition: -webkit-transform 0.6s cubic-bezier(0.49, 0.23, 0.58, 0.49);\n\t-o-transition: transform 0.6s cubic-bezier(0.49, 0.23, 0.58, 0.49);\n\ttransition: transform 0.6s cubic-bezier(0.49, 0.23, 0.58, 0.49);\n\ttransition: transform 0.6s cubic-bezier(0.49, 0.23, 0.58, 0.49), -webkit-transform 0.6s cubic-bezier(0.49, 0.23, 0.58, 0.49);\n}\n\n.dark {\n\t-webkit-box-sizing: border-box;\n\t box-sizing: border-box;\n\t-webkit-backface-visibility: hidden;\n\t\t\t\t\tbackface-visibility: hidden;\n\tbackground: -webkit-repeating-linear-gradient(315deg, rgba(0, 0, 0, 0.3), transparent 1px, rgba(0, 0, 0, 0.3) 2px);\n\tbackground: -o-repeating-linear-gradient(315deg, rgba(0, 0, 0, 0.3), transparent 1px, rgba(0, 0, 0, 0.3) 2px);\n\tbackground: repeating-linear-gradient(135deg, rgba(0, 0, 0, 0.3), transparent 1px, rgba(0, 0, 0, 0.3) 2px);\n\tbackground-size: 3px 3px;\n\tbackground-color: #302f34;\n\tborder-radius: 1px;\n\t-webkit-box-shadow: 0px -6px 8px 0px rgba(0, 0, 0, 0.1), 0px 6px 8px 0px rgba(0, 0, 0, 0.1), 6px 0px 8px 0px rgba(0, 0, 0, 0.1), -6px 0px 8px 0px rgba(0, 0, 0, 0.1);\n\t box-shadow: 0px -6px 8px 0px rgba(0, 0, 0, 0.1), 0px 6px 8px 0px rgba(0, 0, 0, 0.1), 6px 0px 8px 0px rgba(0, 0, 0, 0.1), -6px 0px 8px 0px rgba(0, 0, 0, 0.1);\n\tcursor: pointer;\n\tpadding: 5%;\n}\n\n.commit-hash {\n\tposition: absolute;\n\tright: 5px;\n\tbottom: 5px;\n\tcolor: rgba(197, 197, 197, 0.66);\n}\n\n.keep-aspect-ratio {\n\tposition: absolute;\n\ttop: 2px;\n\tright: 7px;\n\theight: 60px;\n\tpadding-top: 1px;\n\tfont-size: 0.8em;\n}\n\na {\n\tcolor: #FFF;\n}\n\na:hover, a:visited, a:link, a:active\n{\n text-decoration: none;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./example/App.css",".minimap-viewport{position:absolute;-webkit-box-sizing:border-box;box-sizing:border-box;background-color:rgba(0,0,0,.4);z-index:1;cursor:move}.minimap{float:right;position:fixed;right:0;z-index:1;margin:10px;background-color:rgba(0,0,0,.29);border:1px solid rgba(0,0,0,.17)}.minimap-container{overflow:scroll;width:100%;height:100%}.minimap-children{background:#ccc;border:1px solid #000;-webkit-box-sizing:border-box;box-sizing:border-box}\n\n\n// WEBPACK FOOTER //\n// ./dist/react-minimap.css"],"sourceRoot":""} -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce((env, key) => { 82 | env[key] = JSON.stringify(raw[key]); 83 | return env; 84 | }, {}), 85 | }; 86 | 87 | return { raw, stringified }; 88 | } 89 | 90 | module.exports = getClientEnvironment; 91 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = 'build/' //process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-minimap", 3 | "version": "0.8.1-alpha-3", 4 | "description": "A minimap for React", 5 | "main": "dist/react-minimap.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jeremy-carbonne/react-minimap.git" 9 | }, 10 | "keywords": [ 11 | "react-minimap", 12 | "minimap", 13 | "react-component" 14 | ], 15 | "author": "Jérémy CARBONNE", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/jeremy-carbonne/react-minimap/issues" 19 | }, 20 | "homepage": "https://github.com/jeremy-carbonne/react-minimap", 21 | "dependencies": { 22 | "lodash": "^4.17.4" 23 | }, 24 | "peerDependencies": { 25 | "react": "^15.x || ^16.x" 26 | }, 27 | "devDependencies": { 28 | "autoprefixer": "7.1.2", 29 | "babel-cli": "^6.26.0", 30 | "babel-core": "6.25.0", 31 | "babel-eslint": "7.2.3", 32 | "babel-jest": "20.0.3", 33 | "babel-loader": "7.1.1", 34 | "babel-plugin-transform-runtime": "^6.23.0", 35 | "babel-preset-es2015": "^6.24.1", 36 | "babel-preset-react": "^6.24.1", 37 | "babel-preset-react-app": "^3.0.2", 38 | "babel-preset-stage-0": "^6.24.1", 39 | "babel-runtime": "6.26.0", 40 | "case-sensitive-paths-webpack-plugin": "2.1.1", 41 | "chalk": "1.1.3", 42 | "css-loader": "0.28.4", 43 | "dotenv": "4.0.0", 44 | "enzyme": "^3.0.0", 45 | "enzyme-adapter-react-16": "^1.0.0", 46 | "eslint": "4.4.1", 47 | "eslint-config-react-app": "^2.0.0", 48 | "eslint-loader": "1.9.0", 49 | "eslint-plugin-flowtype": "2.35.0", 50 | "eslint-plugin-import": "2.7.0", 51 | "eslint-plugin-jsx-a11y": "5.1.1", 52 | "eslint-plugin-react": "7.1.0", 53 | "extract-text-webpack-plugin": "3.0.0", 54 | "file-loader": "0.11.2", 55 | "fs-extra": "3.0.1", 56 | "html-webpack-plugin": "2.29.0", 57 | "jest": "^21.2.1", 58 | "object-assign": "4.1.1", 59 | "postcss-flexbugs-fixes": "3.2.0", 60 | "postcss-loader": "2.0.6", 61 | "promise": "8.0.1", 62 | "react": "^16.0.0", 63 | "react-dev-utils": "^4.0.0", 64 | "react-dom": "^16.0.0", 65 | "react-test-renderer": "^16.0.0", 66 | "sinon": "^4.0.0", 67 | "style-loader": "0.18.2", 68 | "sw-precache-webpack-plugin": "0.11.4", 69 | "url-loader": "0.5.9", 70 | "webpack": "3.5.1", 71 | "webpack-dev-server": "2.7.1", 72 | "webpack-manifest-plugin": "1.2.1", 73 | "whatwg-fetch": "2.0.3" 74 | }, 75 | "scripts": { 76 | "start": "node scripts/start.js", 77 | "build-example": "node scripts/build.js", 78 | "build": "rm -rf dist/* && node_modules/webpack/bin/webpack.js -p --config webpack.production.config.js", 79 | "test": "node scripts/test.js --env=jsdom" 80 | }, 81 | "jest": { 82 | "collectCoverageFrom": [ 83 | "src/**/*.{js,jsx}" 84 | ], 85 | "setupFiles": [ 86 | "/config/polyfills.js" 87 | ], 88 | "testMatch": [ 89 | "/src/**/__tests__/**/*.js?(x)", 90 | "/src/**/?(*.)(spec|test).js?(x)" 91 | ], 92 | "testEnvironment": "node", 93 | "testURL": "http://localhost", 94 | "transform": { 95 | "^.+\\.(js|jsx)$": "/node_modules/babel-jest", 96 | "^.+\\.css$": "/config/jest/cssTransform.js", 97 | "^(?!.*\\.(js|jsx|css|json)$)": "/config/jest/fileTransform.js" 98 | }, 99 | "transformIgnorePatterns": [ 100 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" 101 | ], 102 | "moduleNameMapper": { 103 | "^react-native$": "react-native-web" 104 | }, 105 | "moduleFileExtensions": [ 106 | "web.js", 107 | "js", 108 | "json", 109 | "web.jsx", 110 | "jsx", 111 | "node" 112 | ] 113 | }, 114 | "babel": { 115 | "presets": [ 116 | "react-app" 117 | ] 118 | }, 119 | "eslintConfig": { 120 | "extends": "react-app" 121 | }, 122 | "files": [ 123 | "package.json", 124 | "dist/react-minimap.js", 125 | "dist/react-minimap.css" 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremy-carbonne/react-minimap/22cd3da355187ff3269498c11d517d8947d60e5f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React minimap 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | const printBuildError = require('react-dev-utils/printBuildError'); 28 | 29 | const measureFileSizesBeforeBuild = 30 | FileSizeReporter.measureFileSizesBeforeBuild; 31 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | 34 | // These sizes are pretty large. We'll warn for bundles exceeding them. 35 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 36 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // First, read the current file sizes in build directory. 44 | // This lets us display how much they changed later. 45 | measureFileSizesBeforeBuild(paths.appBuild) 46 | .then(previousFileSizes => { 47 | // Remove all content but keep the directory so that 48 | // if you're in it, you don't end up in Trash 49 | fs.emptyDirSync(paths.appBuild); 50 | // Merge with the public folder 51 | copyPublicFolder(); 52 | // Start the webpack build 53 | return build(previousFileSizes); 54 | }) 55 | .then( 56 | ({ stats, previousFileSizes, warnings }) => { 57 | if (warnings.length) { 58 | console.log(chalk.yellow('Compiled with warnings.\n')); 59 | console.log(warnings.join('\n\n')); 60 | console.log( 61 | '\nSearch for the ' + 62 | chalk.underline(chalk.yellow('keywords')) + 63 | ' to learn more about each warning.' 64 | ); 65 | console.log( 66 | 'To ignore, add ' + 67 | chalk.cyan('// eslint-disable-next-line') + 68 | ' to the line before.\n' 69 | ); 70 | } else { 71 | console.log(chalk.green('Compiled successfully.\n')); 72 | } 73 | 74 | console.log('File sizes after gzip:\n'); 75 | printFileSizesAfterBuild( 76 | stats, 77 | previousFileSizes, 78 | paths.appBuild, 79 | WARN_AFTER_BUNDLE_GZIP_SIZE, 80 | WARN_AFTER_CHUNK_GZIP_SIZE 81 | ); 82 | console.log(); 83 | 84 | const appPackage = require(paths.appPackageJson); 85 | const publicUrl = paths.publicUrl; 86 | const publicPath = config.output.publicPath; 87 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 88 | printHostingInstructions( 89 | appPackage, 90 | publicUrl, 91 | publicPath, 92 | buildFolder, 93 | useYarn 94 | ); 95 | }, 96 | err => { 97 | console.log(chalk.red('Failed to compile.\n')); 98 | printBuildError(err); 99 | process.exit(1); 100 | } 101 | ); 102 | 103 | // Create the production build and print the deployment instructions. 104 | function build(previousFileSizes) { 105 | console.log('Creating an optimized production build...'); 106 | 107 | let compiler = webpack(config); 108 | return new Promise((resolve, reject) => { 109 | compiler.run((err, stats) => { 110 | if (err) { 111 | return reject(err); 112 | } 113 | const messages = formatWebpackMessages(stats.toJson({}, true)); 114 | if (messages.errors.length) { 115 | // Only keep the first error. Others are often indicative 116 | // of the same problem, but confuse the reader with noise. 117 | if (messages.errors.length > 1) { 118 | messages.errors.length = 1; 119 | } 120 | return reject(new Error(messages.errors.join('\n\n'))); 121 | } 122 | if ( 123 | process.env.CI && 124 | (typeof process.env.CI !== 'string' || 125 | process.env.CI.toLowerCase() !== 'false') && 126 | messages.warnings.length 127 | ) { 128 | console.log( 129 | chalk.yellow( 130 | '\nTreating warnings as errors because process.env.CI = true.\n' + 131 | 'Most CI servers set it automatically.\n' 132 | ) 133 | ); 134 | return reject(new Error(messages.warnings.join('\n\n'))); 135 | } 136 | return resolve({ 137 | stats, 138 | previousFileSizes, 139 | warnings: messages.warnings, 140 | }); 141 | }); 142 | }); 143 | } 144 | 145 | function copyPublicFolder() { 146 | fs.copySync(paths.appPublic, paths.appBuild, { 147 | dereference: true, 148 | filter: file => file !== paths.appHtml, 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const fs = require('fs'); 18 | const chalk = require('chalk'); 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const clearConsole = require('react-dev-utils/clearConsole'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const { 24 | choosePort, 25 | createCompiler, 26 | prepareProxy, 27 | prepareUrls, 28 | } = require('react-dev-utils/WebpackDevServerUtils'); 29 | const openBrowser = require('react-dev-utils/openBrowser'); 30 | const paths = require('../config/paths'); 31 | const config = require('../config/webpack.config.dev'); 32 | const createDevServerConfig = require('../config/webpackDevServer.config'); 33 | 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | const isInteractive = process.stdout.isTTY; 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1); 40 | } 41 | 42 | // Tools like Cloud9 rely on this. 43 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 44 | const HOST = process.env.HOST || '0.0.0.0'; 45 | 46 | // We attempt to use the default port but if it is busy, we offer the user to 47 | // run on a different port. `detect()` Promise resolves to the next free port. 48 | choosePort(HOST, DEFAULT_PORT) 49 | .then(port => { 50 | if (port == null) { 51 | // We have not found a port. 52 | return; 53 | } 54 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 55 | const appName = require(paths.appPackageJson).name; 56 | const urls = prepareUrls(protocol, HOST, port); 57 | // Create a webpack compiler that is configured with custom messages. 58 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 59 | // Load proxy config 60 | const proxySetting = require(paths.appPackageJson).proxy; 61 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 62 | // Serve webpack assets generated by the compiler over a web sever. 63 | const serverConfig = createDevServerConfig( 64 | proxyConfig, 65 | urls.lanUrlForConfig 66 | ); 67 | const devServer = new WebpackDevServer(compiler, serverConfig); 68 | // Launch WebpackDevServer. 69 | devServer.listen(port, HOST, err => { 70 | if (err) { 71 | return console.log(err); 72 | } 73 | if (isInteractive) { 74 | clearConsole(); 75 | } 76 | console.log(chalk.cyan('Starting the development server...\n')); 77 | openBrowser(urls.localUrlForBrowser); 78 | }); 79 | 80 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 81 | process.on(sig, function() { 82 | devServer.close(); 83 | process.exit(); 84 | }); 85 | }); 86 | }) 87 | .catch(err => { 88 | if (err && err.message) { 89 | console.log(err.message); 90 | } 91 | process.exit(1); 92 | }); 93 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /src/__tests__/Minimap.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Minimap from '../react-minimap' 3 | import { render, configure } from 'enzyme' 4 | import Adapter from 'enzyme-adapter-react-16' 5 | import sinon from 'sinon' 6 | 7 | configure({ adapter: new Adapter() }) 8 | 9 | describe('Minimap', () => { 10 | 11 | it('should mount without any warning', () => { 12 | // react writes to console.error on a PropType error :( 13 | const errors = [] 14 | sinon.stub(console, 'error').callsFake(error => { 15 | errors.push(error) 16 | }) 17 | 18 | render( 19 |
20 | 21 |
    22 |
  • Hello Minimap
  • 23 |
  • Hello World
  • 24 |
  • Hello Blah
  • 25 |
26 |
27 |
28 | ) 29 | expect(errors).toEqual([]) 30 | }) 31 | 32 | }) -------------------------------------------------------------------------------- /src/components/Child.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import '../react-minimap.css' 4 | 5 | 6 | export class Child extends React.Component { 7 | static propTypes = { 8 | width: PropTypes.number.isRequired, 9 | height: PropTypes.number.isRequired, 10 | top: PropTypes.number.isRequired, 11 | left: PropTypes.number.isRequired, 12 | node: PropTypes.any, 13 | }; 14 | 15 | render() { 16 | const {width, height, left, top} = this.props 17 | return ( 18 |
28 | ); 29 | } 30 | } 31 | 32 | export default Child 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default Minimap from './react-minimap' 2 | export {default as Child} from './components/Child' -------------------------------------------------------------------------------- /src/react-minimap.css: -------------------------------------------------------------------------------- 1 | .minimap-viewport { 2 | position: absolute; 3 | box-sizing: border-box; 4 | background-color: rgba(0, 0, 0, 0.4); 5 | z-index: 1; 6 | cursor: move; 7 | } 8 | 9 | .minimap { 10 | float: right; 11 | position: fixed; 12 | right: 0; 13 | z-index: 1; 14 | margin: 10px; 15 | background-color: rgba(0, 0, 0, 0.29); 16 | border: 1px solid rgba(0, 0, 0, 0.17); 17 | } 18 | 19 | .minimap-container { 20 | width: 100%; 21 | height: 100%; 22 | } 23 | 24 | .minimap-container-scroll { 25 | overflow: scroll; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .minimap-children { 31 | background: #CCC; 32 | border: 1px solid black; 33 | box-sizing: border-box; 34 | } -------------------------------------------------------------------------------- /src/react-minimap.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import _ from 'lodash' 3 | import PropTypes from 'prop-types' 4 | import Child from './components/Child' 5 | import './react-minimap.css' 6 | 7 | export class Minimap extends React.Component { 8 | static propTypes = { 9 | className: PropTypes.string, 10 | selector: PropTypes.string.isRequired, 11 | width: PropTypes.number /** in pixel */, 12 | height: PropTypes.number /** in pixel */, 13 | keepAspectRatio: PropTypes.bool, 14 | childComponent: PropTypes.any, 15 | onMountCenterOnX: PropTypes.bool, 16 | onMountCenterOnY: PropTypes.bool, 17 | } 18 | 19 | static defaultProps = { 20 | className: '', 21 | width: 200, 22 | height: 200, 23 | keepAspectRatio: false, 24 | childComponent: Child, 25 | onMountCenterOnX: false, 26 | onMountCenterOnY: false, 27 | } 28 | 29 | constructor(props) { 30 | super(props) 31 | this.down = this.down.bind(this) 32 | this.move = this.move.bind(this) 33 | this.synchronize = this.synchronize.bind(this) 34 | this.init = this.init.bind(this) 35 | this.up = this.up.bind(this) 36 | 37 | this.resize = _.throttle(this.synchronize, 100) 38 | 39 | this.state = { 40 | children: null, 41 | viewport: null, 42 | width: props.width, 43 | height: props.height, 44 | } 45 | 46 | this.downState = false 47 | this.initState = false 48 | } 49 | 50 | componentDidMount() { 51 | const { onMountCenterOnX, onMountCenterOnY } = this.props 52 | setTimeout(() => 53 | this.synchronize({ 54 | centerOnX: onMountCenterOnX, 55 | centerOnY: onMountCenterOnY, 56 | }), 57 | ) 58 | window.addEventListener('resize', this.resize) 59 | this.init() 60 | } 61 | 62 | componentWillUnmount() { 63 | window.removeEventListener('resize', this.resize) 64 | } 65 | 66 | componentWillReceiveProps(nextProps) { 67 | if (nextProps.keepAspectRatio !== this.props.keepAspectRatio) { 68 | setTimeout(this.synchronize); 69 | } else if (nextProps.children !== this.props.children) { 70 | setTimeout(this.synchronize); 71 | } 72 | } 73 | 74 | componentDidUpdate() { 75 | if (this.initState) { 76 | this.initState = false 77 | } else { 78 | this.initState = true 79 | this.init() 80 | } 81 | } 82 | 83 | init() { 84 | const {childComponent, keepAspectRatio} = this.props; 85 | const ChildComponent = childComponent; 86 | const {scrollWidth, scrollHeight, scrollTop, scrollLeft} = this.source; 87 | const sourceRect = this.source.getBoundingClientRect(); 88 | 89 | let { width, height } = this.props 90 | 91 | let ratioX = width / scrollWidth 92 | let ratioY = height / scrollHeight 93 | 94 | if (keepAspectRatio) { 95 | if (ratioX < ratioY) { 96 | ratioY = ratioX 97 | height = Math.round(scrollHeight / (scrollWidth / width)) 98 | } else { 99 | ratioX = ratioY 100 | width = Math.round(scrollWidth / (scrollHeight / height)) 101 | } 102 | } 103 | 104 | const nodes = this.source.querySelectorAll(this.props.selector) 105 | this.setState({ 106 | ...this.state, 107 | height, 108 | width, 109 | children: _.map(nodes, (node, key) => { 110 | const { width, height, left, top } = node.getBoundingClientRect() 111 | 112 | const wM = width * ratioX 113 | const hM = height * ratioY 114 | const xM = (left + scrollLeft - sourceRect.left) * ratioX 115 | const yM = (top + scrollTop - sourceRect.top) * ratioY 116 | 117 | return ( 118 | 126 | ) 127 | }), 128 | }) 129 | } 130 | 131 | down(e) { 132 | const pos = this.minimap.getBoundingClientRect() 133 | 134 | this.x = Math.round(pos.left + this.l + this.w / 2) 135 | this.y = Math.round(pos.top + this.t + this.h / 2) 136 | 137 | this.downState = true 138 | this.move(e) 139 | } 140 | 141 | up() { 142 | this.downState = false 143 | } 144 | 145 | move(e) { 146 | if (this.downState === false) return 147 | 148 | const { width, height } = this.state 149 | let event 150 | 151 | e.preventDefault() 152 | if (e.type.match(/touch/)) { 153 | if (e.touches.length > 1) { 154 | return 155 | } 156 | event = e.touches[0] 157 | } else { 158 | event = e 159 | } 160 | 161 | let dx = event.clientX - this.x 162 | let dy = event.clientY - this.y 163 | if (this.l + dx < 0) { 164 | dx = -this.l 165 | } 166 | if (this.t + dy < 0) { 167 | dy = -this.t 168 | } 169 | if (this.l + this.w + dx > width) { 170 | dx = width - this.l - this.w 171 | } 172 | if (this.t + this.h + dy > height) { 173 | dy = height - this.t - this.h 174 | } 175 | 176 | this.x += dx 177 | this.y += dy 178 | 179 | this.l += dx 180 | this.t += dy 181 | 182 | // Sanity checks: 183 | this.l = this.l < 0 ? 0 : this.l 184 | this.t = this.t < 0 ? 0 : this.t 185 | 186 | const coefX = width / this.source.scrollWidth 187 | const coefY = height / this.source.scrollHeight 188 | const left = this.l / coefX 189 | const top = this.t / coefY 190 | 191 | this.source.scrollLeft = Math.round(left) 192 | this.source.scrollTop = Math.round(top) 193 | this.redraw() 194 | } 195 | 196 | synchronize(options) { 197 | const { width, height } = this.state 198 | 199 | const rect = this.source.getBoundingClientRect() 200 | 201 | const dims = [rect.width, rect.height] 202 | const scroll = [this.source.scrollLeft, this.source.scrollTop] 203 | const scaleX = width / this.source.scrollWidth 204 | const scaleY = height / this.source.scrollHeight 205 | 206 | const lW = dims[0] * scaleX 207 | const lH = dims[1] * scaleY 208 | const lX = scroll[0] * scaleX 209 | const lY = scroll[1] * scaleY 210 | 211 | // Ternary operation includes sanity check 212 | this.w = Math.round(lW) > this.state.width 213 | ? this.state.width 214 | : Math.round(lW) 215 | this.h = Math.round(lH) > this.state.height 216 | ? this.state.height 217 | : Math.round(lH) 218 | this.l = Math.round(lX) 219 | this.t = Math.round(lY) 220 | 221 | if (options !== undefined) { 222 | if (options.centerOnX === true) { 223 | this.source.scrollLeft = this.source.scrollWidth / 2 - dims[0] / 2 224 | } 225 | 226 | if (options.centerOnY === true) { 227 | this.source.scrollTop = this.source.scrollHeight / 2 - dims[1] / 2 228 | } 229 | } 230 | 231 | this.redraw() 232 | } 233 | 234 | redraw() { 235 | this.setState({ 236 | ...this.state, 237 | viewport: ( 238 |
247 | ), 248 | }) 249 | } 250 | 251 | render() { 252 | const { width, height } = this.state 253 | 254 | return ( 255 |
256 |
{ 263 | this.minimap = minimap 264 | }} 265 | onMouseDown={this.down} 266 | onTouchStart={this.down} 267 | onTouchMove={this.move} 268 | onMouseMove={this.move} 269 | onTouchEnd={this.up} 270 | onMouseUp={this.up} 271 | > 272 | {this.state.viewport} 273 | {this.state.children} 274 |
275 | 276 |
{ 280 | this.source = container 281 | }} 282 | > 283 | {this.props.children} 284 |
285 |
286 | ) 287 | } 288 | } 289 | 290 | export default Minimap 291 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 3 | /** 4 | * This is the Webpack configuration file for production. 5 | */ 6 | module.exports = { 7 | entry: "./src/react-minimap", 8 | 9 | output: { 10 | library: 'ReactEmoji', 11 | libraryTarget: 'umd', 12 | path: __dirname + "/dist/", 13 | filename: "react-minimap.js" 14 | }, 15 | 16 | externals: [ 17 | { 18 | "react": { 19 | root: "React", 20 | commonjs2: "react", 21 | commonjs: "react", 22 | amd: "react" 23 | } 24 | } 25 | ], 26 | 27 | module: { 28 | rules: [ 29 | { test: /\.js?$/, exclude: /node_modules/, use: "babel-loader" }, 30 | { test: /\.css?$/, exclude: /node_modules/, use: ExtractTextPlugin.extract(["css-loader"]) } 31 | ] 32 | }, 33 | 34 | resolve: { 35 | extensions: ['.js', '.jsx'] 36 | }, 37 | 38 | plugins: [ 39 | new ExtractTextPlugin("./react-minimap.css"), 40 | ] 41 | }; 42 | --------------------------------------------------------------------------------