├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── idle-maps.js └── idle-maps.min.js ├── examples ├── index.html ├── index.jsx ├── map-component.jsx └── overlay-component.jsx ├── index.js ├── lib ├── google-maps-mixin.js ├── map-instance-mixin.jsx └── overlay-mixin.js ├── package.json ├── test ├── google-maps-mixin.spec.js ├── helpers │ └── jsdom.js ├── map-instance-mixin.spec.js └── overlay-mixin.spec.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [package.json] 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | examples/app.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true, 5 | "mocha": true, 6 | "es6": true 7 | }, 8 | "ecmaFeatures": { 9 | "modules": true 10 | }, 11 | "rules": { 12 | "no-alert": 2, 13 | "no-array-constructor": 2, 14 | "no-caller": 2, 15 | "no-bitwise": 2, 16 | "no-catch-shadow": 2, 17 | "no-console": 2, 18 | "no-comma-dangle": 2, 19 | "no-control-regex": 2, 20 | "no-debugger": 2, 21 | "no-div-regex": 2, 22 | "no-dupe-keys": 2, 23 | "no-else-return": 2, 24 | "no-empty": 2, 25 | "no-empty-class": 2, 26 | "no-eq-null": 2, 27 | "no-eval": 2, 28 | "no-ex-assign": 2, 29 | "no-extra-semi": 2, 30 | "no-func-assign": 2, 31 | "no-floating-decimal": 2, 32 | "no-implied-eval": 2, 33 | "no-with": 2, 34 | "no-fallthrough": 2, 35 | "no-unreachable": 2, 36 | "no-undef": 2, 37 | "no-undef-init": 2, 38 | "no-unused-expressions": 2, 39 | "no-octal": 2, 40 | "no-octal-escape": 2, 41 | "no-obj-calls": 2, 42 | "no-multi-str": 2, 43 | "no-new-wrappers": 2, 44 | "no-new": 2, 45 | "no-new-func": 2, 46 | "no-native-reassign": 2, 47 | "no-plusplus": 2, 48 | "no-delete-var": 2, 49 | "no-return-assign": 2, 50 | "no-new-object": 2, 51 | "no-label-var": 2, 52 | "no-ternary": 0, 53 | "no-self-compare": 2, 54 | "no-sync": 2, 55 | "no-underscore-dangle": 2, 56 | "no-loop-func": 2, 57 | "no-empty-label": 2, 58 | "no-unused-vars": 2, 59 | "no-script-url": 2, 60 | "no-proto": 2, 61 | "no-iterator": 2, 62 | "no-mixed-requires": [0, false], 63 | "no-wrap-func": 2, 64 | "no-shadow": 2, 65 | "no-use-before-define": 2, 66 | "no-redeclare": 2, 67 | "no-regex-spaces": 2, 68 | "brace-style": [2, "1tbs"], 69 | "block-scoped-var": 0, 70 | "camelcase": 2, 71 | "complexity": [0, 4], 72 | "curly": 2, 73 | "dot-notation": 2, 74 | "eqeqeq": 2, 75 | "global-strict": 0, 76 | "guard-for-in": 0, 77 | "max-depth": [0, 4], 78 | "max-len": [0, 80, 4], 79 | "max-params": [0, 3], 80 | "max-statements": [0, 10], 81 | "new-cap": 2, 82 | "new-parens": 2, 83 | "one-var": 2, 84 | "quotes": [2, "single"], 85 | "quote-props": 0, 86 | "radix": 0, 87 | "semi": 2, 88 | "space-after-keywords": [2, "always", { "checkFunctionKeyword": true } ], 89 | "space-before-blocks": [2, "always"], 90 | "strict": 2, 91 | "unnecessary-strict": 0, 92 | "use-isnan": 2, 93 | "valid-typeof": 0, 94 | "wrap-iife": 2, 95 | "wrap-regex": 0, 96 | "yoda": "always" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | examples/app.js 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | test 3 | .idea 4 | .travis.yml 5 | .editorconfig 6 | .eslintrc 7 | .eslintignore 8 | .gitignore 9 | webpack.config.js 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "iojs" 4 | env: 5 | global: 6 | - NODE_ENV=travisci 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Zach Pratt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React components for [Async Google Maps](https://github.com/zpratt/async-google-maps) 2 | 3 | [![Build Status](https://travis-ci.org/zpratt/idle-maps.svg)](https://travis-ci.org/zpratt/idle-maps) 4 | [![npm](https://img.shields.io/npm/v/idle-maps.svg)](https://www.npmjs.com/package/idle-maps) 5 | 6 | ### Not fully baked yet 7 | 8 | I am still working out some things with the overlay mixin. 9 | 10 | ## Running tests 11 | 12 | This module is 100% test-driven. Please feel free to run the tests and critique them. 13 | 14 | 1. `git clone` ... 15 | 2. Install io.js (needed for jsdom) 16 | 3. `npm i` 17 | 4. `npm test` 18 | 19 | ## Usage 20 | 21 | Take a look at the examples directory for a few samples of how to use the mixins. 22 | 23 | ### Running examples locally 24 | 25 | 1. `npm i` 26 | 2. `npm start` 27 | 3. Navigate to [http://localhost:8080/](http://localhost:8080/) 28 | 29 | ## Documentation 30 | 31 | Coming soon. 32 | -------------------------------------------------------------------------------- /dist/idle-maps.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("react")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["react"], factory); 6 | else if(typeof exports === 'object') 7 | exports["IdleMaps"] = factory(require("react")); 8 | else 9 | root["IdleMaps"] = factory(root["React"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_5__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | 'use strict'; 58 | 59 | var GoogleMapsApiMixin = __webpack_require__(1), 60 | MapInstanceMixin = __webpack_require__(3), 61 | OverlayViewMixin = __webpack_require__(2); 62 | 63 | module.exports = { 64 | GoogleMapsApiMixin: GoogleMapsApiMixin, 65 | MapInstanceMixin: MapInstanceMixin, 66 | OverlayViewMixin: OverlayViewMixin 67 | }; 68 | 69 | 70 | /***/ }, 71 | /* 1 */ 72 | /***/ function(module, exports, __webpack_require__) { 73 | 74 | 'use strict'; 75 | 76 | var MapLoader = __webpack_require__(4).MapLoader; 77 | 78 | module.exports = { 79 | componentWillMount: function () { 80 | MapLoader.load({ 81 | key: this.props.apiKey, 82 | version: this.props.apiVersion 83 | }); 84 | } 85 | }; 86 | 87 | 88 | /***/ }, 89 | /* 2 */ 90 | /***/ function(module, exports, __webpack_require__) { 91 | 92 | 'use strict'; 93 | 94 | var OverlayFactory = __webpack_require__(4).BaseOverlayFactory; 95 | 96 | module.exports = { 97 | componentDidMount: function () { 98 | var element = this.getDOMNode(); 99 | 100 | OverlayFactory.create({ 101 | point: this.props.point, 102 | el: element, 103 | map: this.props.map 104 | }); 105 | } 106 | }; 107 | 108 | 109 | /***/ }, 110 | /* 3 */ 111 | /***/ function(module, exports, __webpack_require__) { 112 | 113 | /** 114 | * @jsx React.DOM 115 | */ 116 | 117 | 'use strict'; 118 | 119 | var MapLoader = __webpack_require__(4).MapLoader, 120 | 121 | React = __webpack_require__(5); 122 | 123 | module.exports = { 124 | componentDidMount: function () { 125 | MapLoader.create(this.getDOMNode(), this.props.mapOptions).then(function (map) { 126 | this.idle(map); 127 | }.bind(this)); 128 | }, 129 | render: function () { 130 | return ( 131 | React.createElement("div", {className: "map-container"}, 132 | React.createElement("div", {className: "test"}) 133 | ) 134 | ); 135 | } 136 | }; 137 | 138 | 139 | /***/ }, 140 | /* 4 */ 141 | /***/ function(module, exports, __webpack_require__) { 142 | 143 | 'use strict'; 144 | 145 | /** 146 | * @author zach pratt 147 | * @licence MIT 148 | * @module async-google-maps 149 | */ 150 | 151 | var MapLoader = __webpack_require__(6), 152 | BaseOverlayFactory = __webpack_require__(7); 153 | 154 | module.exports = { 155 | /** 156 | * See {@link async-google-maps/map-loader} 157 | * */ 158 | MapLoader: MapLoader, 159 | /** 160 | * See {@link async-google-maps/base-overlay-factory} 161 | * */ 162 | BaseOverlayFactory: BaseOverlayFactory 163 | }; 164 | 165 | 166 | /***/ }, 167 | /* 5 */ 168 | /***/ function(module, exports, __webpack_require__) { 169 | 170 | module.exports = __WEBPACK_EXTERNAL_MODULE_5__; 171 | 172 | /***/ }, 173 | /* 6 */ 174 | /***/ function(module, exports, __webpack_require__) { 175 | 176 | /* WEBPACK VAR INJECTION */(function(global) {'use strict'; 177 | 178 | /** 179 | * @module async-google-maps/map-loader 180 | */ 181 | 182 | var URL_PREFIX = '//maps.googleapis.com/maps/api/js', 183 | CALLBACK_IDENTIFIER = 'mapLoaded', 184 | 185 | mapLoadedDeferred, 186 | mapLoadedPromise; 187 | 188 | global[CALLBACK_IDENTIFIER] = function mapLoaded() { 189 | mapLoadedDeferred.resolve(); 190 | }; 191 | 192 | function buildUrl(options) { 193 | return URL_PREFIX 194 | + '?v=' + options.version 195 | + '&key=' + options.key 196 | + '&callback=' + CALLBACK_IDENTIFIER; 197 | } 198 | 199 | function createDeferredToAllowForResolutionAfterGoogleMapsLoaderFinishes(resolve, reject) { 200 | mapLoadedDeferred = { 201 | resolve: resolve, 202 | reject: reject 203 | }; 204 | } 205 | 206 | function createMapLoadedPromise() { 207 | mapLoadedPromise = new Promise(function (resolve, reject) { 208 | createDeferredToAllowForResolutionAfterGoogleMapsLoaderFinishes(resolve, reject); 209 | }); 210 | } 211 | 212 | function appendScriptOnDocumentReady(scriptEl) { 213 | document.addEventListener('DOMContentLoaded', function () { 214 | document.head.appendChild(scriptEl); 215 | }); 216 | } 217 | 218 | function injectGoogleMapsScript(options) { 219 | var scriptEl = document.createElement('script'); 220 | scriptEl.src = buildUrl(options); 221 | scriptEl.type = 'text/javascript'; 222 | 223 | appendScriptOnDocumentReady(scriptEl); 224 | } 225 | 226 | function init() { 227 | createMapLoadedPromise(); 228 | } 229 | 230 | init(); 231 | 232 | module.exports = { 233 | /** 234 | * Asynchronously loads the google maps javascript library, given the supplied options. 235 | * Returns a promise that will be resolved once the google maps loader has finished. 236 | * Once the promise resolves, it is safe to reference anything under the `google.maps` namespace. 237 | * This method should only be called once for a given application. 238 | * 239 | * @param {Object} options - The configuration used to load the google maps API 240 | * @param {string} options.version - The version of the google maps API to load 241 | * @param {string} options.key - The API key of the consuming application 242 | * @returns {Promise} 243 | * */ 244 | load: function loadGoogleMapsAsync(options) { 245 | injectGoogleMapsScript(options); 246 | 247 | return mapLoadedPromise; 248 | }, 249 | 250 | /** 251 | * @property {Promise} loaded - A reference to a promise that is resolved once the google maps API is ready. 252 | */ 253 | loaded: (function () { 254 | return mapLoadedPromise; 255 | }()), 256 | 257 | /** 258 | * Creates a map instance given the supplied options. The options will be passed into the `google.maps.Map` constructor, 259 | * therefore, all options from the [google maps api](https://developers.google.com/maps/documentation/javascript/reference#MapOptions) can be used. 260 | * This function returns a promise which will be resolved once the newly created map instance is in the `idle` state, 261 | * which is the point at which overlays, markers, and geometries can be added to the map. 262 | * 263 | * @param mapContainer - The element to attach the map instance to 264 | * @param options - Options passed to the google Map constructor 265 | * @returns {Promise} 266 | * */ 267 | create: function createMap(mapContainer, options) { 268 | return new Promise(function mapIdleResolver(resolve) { 269 | mapLoadedPromise.then(function whenMapHasLoaded() { 270 | var googleMaps = __webpack_require__(8), 271 | mapInstance = new googleMaps.maps.Map(mapContainer, options); 272 | 273 | googleMaps.maps.event.addListenerOnce(mapInstance, 'idle', function mapIdleHandler() { 274 | resolve(mapInstance); 275 | }); 276 | }); 277 | }); 278 | } 279 | }; 280 | 281 | /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) 282 | 283 | /***/ }, 284 | /* 7 */ 285 | /***/ function(module, exports, __webpack_require__) { 286 | 287 | 'use strict'; 288 | 289 | /** 290 | * @module async-google-maps/base-overlay-factory 291 | */ 292 | 293 | var MapLoader = __webpack_require__(6); 294 | 295 | module.exports = { 296 | /** 297 | * Creates an instance of a custom overlay that will position itself on the map based on the overlay dimensions 298 | * and the provided point. 299 | * 300 | * @param options - Options passed to the base overlay constructor 301 | * @param options.el - The element that the overlay will use to append into the overlay pane. 302 | * @param options.point - The lat/lng that the overlay will use to position itself on the map. 303 | */ 304 | create: function (options) { 305 | MapLoader.loaded.then(function () { 306 | var BaseOverlay = __webpack_require__(9), 307 | overlayInstance = new BaseOverlay(options); 308 | 309 | overlayInstance.setMap(options.map); 310 | }); 311 | } 312 | }; 313 | 314 | 315 | /***/ }, 316 | /* 8 */ 317 | /***/ function(module, exports, __webpack_require__) { 318 | 319 | /* WEBPACK VAR INJECTION */(function(global) {'use strict'; 320 | 321 | module.exports = global.google; 322 | 323 | /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) 324 | 325 | /***/ }, 326 | /* 9 */ 327 | /***/ function(module, exports, __webpack_require__) { 328 | 329 | /* WEBPACK VAR INJECTION */(function(global) {'use strict'; 330 | 331 | var google = global.google; 332 | 333 | function onAdd() { 334 | var panes = this.getPanes(); 335 | 336 | panes.overlayLayer.appendChild(this.el); 337 | } 338 | 339 | function positionOverlayByDimensions(projectedLatLng) { 340 | var offsetHeight = this.el.offsetHeight, 341 | offsetWidth = this.el.offsetWidth; 342 | 343 | this.el.style.top = projectedLatLng.y - offsetHeight + 'px'; 344 | this.el.style.left = projectedLatLng.x - Math.floor(offsetWidth / 2) + 'px'; 345 | } 346 | 347 | function draw() { 348 | var projection = this.getProjection(), 349 | projectedLatLng = projection.fromLatLngToDivPixel(this.point); 350 | 351 | positionOverlayByDimensions.call(this, projectedLatLng); 352 | } 353 | 354 | function BaseOverlay(options) { 355 | var point = options.point; 356 | 357 | this.el = options.el; 358 | this.point = new google.maps.LatLng(point.lat, point.lng); 359 | 360 | this.el.style.position = 'absolute'; 361 | } 362 | 363 | BaseOverlay.prototype = Object.create(google.maps.OverlayView.prototype); 364 | BaseOverlay.prototype.constructor = BaseOverlay; 365 | 366 | BaseOverlay.prototype.onAdd = onAdd; 367 | BaseOverlay.prototype.draw = draw; 368 | 369 | module.exports = BaseOverlay; 370 | 371 | /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) 372 | 373 | /***/ } 374 | /******/ ]) 375 | }); 376 | ; -------------------------------------------------------------------------------- /dist/idle-maps.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("react")):"function"==typeof define&&define.amd?define(["react"],e):"object"==typeof exports?exports.IdleMaps=e(require("react")):t.IdleMaps=e(t.React)}(this,function(t){return function(t){function e(o){if(n[o])return n[o].exports;var i=n[o]={exports:{},id:o,loaded:!1};return t[o].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";var o=n(1),i=n(3),r=n(2);t.exports={GoogleMapsApiMixin:o,MapInstanceMixin:i,OverlayViewMixin:r}},function(t,e,n){"use strict";var o=n(4).MapLoader;t.exports={componentWillMount:function(){o.load({key:this.props.apiKey,version:this.props.apiVersion})}}},function(t,e,n){"use strict";var o=n(4).BaseOverlayFactory;t.exports={componentDidMount:function(){var t=this.getDOMNode();o.create({point:this.props.point,el:t,map:this.props.map})}}},function(t,e,n){"use strict";var o=n(4).MapLoader,i=n(5);t.exports={componentDidMount:function(){o.create(this.getDOMNode(),this.props.mapOptions).then(function(t){this.idle(t)}.bind(this))},render:function(){return i.createElement("div",{className:"map-container"},i.createElement("div",{className:"test"}))}}},function(t,e,n){"use strict";var o=n(6),i=n(7);t.exports={MapLoader:o,BaseOverlayFactory:i}},function(e){e.exports=t},function(t,e,n){(function(e){"use strict";function o(t){return f+"?v="+t.version+"&key="+t.key+"&callback="+l}function i(t,e){p={resolve:t,reject:e}}function r(){u=new Promise(function(t,e){i(t,e)})}function s(t){document.addEventListener("DOMContentLoaded",function(){document.head.appendChild(t)})}function a(t){var e=document.createElement("script");e.src=o(t),e.type="text/javascript",s(e)}function c(){r()}var p,u,f="//maps.googleapis.com/maps/api/js",l="mapLoaded";e[l]=function(){p.resolve()},c(),t.exports={load:function(t){return a(t),u},loaded:function(){return u}(),create:function(t,e){return new Promise(function(o){u.then(function(){var i=n(8),r=new i.maps.Map(t,e);i.maps.event.addListenerOnce(r,"idle",function(){o(r)})})})}}}).call(e,function(){return this}())},function(t,e,n){"use strict";var o=n(6);t.exports={create:function(t){o.loaded.then(function(){var e=n(9),o=new e(t);o.setMap(t.map)})}}},function(t,e){(function(e){"use strict";t.exports=e.google}).call(e,function(){return this}())},function(t,e){(function(e){"use strict";function n(){var t=this.getPanes();t.overlayLayer.appendChild(this.el)}function o(t){var e=this.el.offsetHeight,n=this.el.offsetWidth;this.el.style.top=t.y-e+"px",this.el.style.left=t.x-Math.floor(n/2)+"px"}function i(){var t=this.getProjection(),e=t.fromLatLngToDivPixel(this.point);o.call(this,e)}function r(t){var e=t.point;this.el=t.el,this.point=new s.maps.LatLng(e.lat,e.lng),this.el.style.position="absolute"}var s=e.google;r.prototype=Object.create(s.maps.OverlayView.prototype),r.prototype.constructor=r,r.prototype.onAdd=n,r.prototype.draw=i,t.exports=r}).call(e,function(){return this}())}])}); -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var MapComponent = require('./map-component.jsx'), 8 | 9 | React = require('react'), 10 | MapElement; 11 | 12 | function createMap() { 13 | var mapOptions = { 14 | center: { 15 | lat: 41.011, 16 | lng: -89.22 17 | }, 18 | zoom: 7 19 | }; 20 | 21 | return ( 22 | 27 | ); 28 | } 29 | 30 | MapElement = createMap(); 31 | 32 | React.render(MapElement, document.querySelector('.content')); 33 | -------------------------------------------------------------------------------- /examples/map-component.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var IdleMaps = require('../dist/idle-maps'), 8 | React = require('react'), 9 | 10 | GoogleMapsApiMixin = IdleMaps.GoogleMapsApiMixin, 11 | MapMixin = IdleMaps.MapInstanceMixin, 12 | 13 | OverlayComponent = require('./overlay-component.jsx'); 14 | 15 | function createOverlay(map) { 16 | var point = { 17 | lat: 40.22, 18 | lng: -90.11 19 | }; 20 | 21 | return ( 22 | 23 | ); 24 | } 25 | 26 | module.exports = React.createClass({ 27 | mixins: [GoogleMapsApiMixin, MapMixin], 28 | idle: function (map) { 29 | var OverlayElement = createOverlay(map); 30 | 31 | React.render(OverlayElement, document.createElement('div')); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /examples/overlay-component.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var IdleMaps = require('../dist/idle-maps'), 8 | React = require('react'), 9 | OverlayMixin = IdleMaps.OverlayViewMixin; 10 | 11 | module.exports = React.createClass({ 12 | mixins: [OverlayMixin], 13 | render: function () { 14 | var overlayStyle = { 15 | backgroundColor: '#FFF', 16 | border: '1px solid #000' 17 | }; 18 | 19 | return ( 20 |
21 |

22 | {this.props.message} 23 |

24 |
25 | ); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var GoogleMapsApiMixin = require('./lib/google-maps-mixin'), 4 | MapInstanceMixin = require('./lib/map-instance-mixin.jsx'), 5 | OverlayViewMixin = require('./lib/overlay-mixin'); 6 | 7 | module.exports = { 8 | GoogleMapsApiMixin: GoogleMapsApiMixin, 9 | MapInstanceMixin: MapInstanceMixin, 10 | OverlayViewMixin: OverlayViewMixin 11 | }; 12 | -------------------------------------------------------------------------------- /lib/google-maps-mixin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MapLoader = require('async-google-maps').MapLoader; 4 | 5 | module.exports = { 6 | componentWillMount: function () { 7 | MapLoader.load({ 8 | key: this.props.apiKey, 9 | version: this.props.apiVersion 10 | }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/map-instance-mixin.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var MapLoader = require('async-google-maps').MapLoader, 8 | 9 | React = require('react'); 10 | 11 | module.exports = { 12 | componentDidMount: function () { 13 | MapLoader.create(this.getDOMNode(), this.props.mapOptions).then(function (map) { 14 | this.idle(map); 15 | }.bind(this)); 16 | }, 17 | render: function () { 18 | return ( 19 |
20 |
21 | ); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/overlay-mixin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var OverlayFactory = require('async-google-maps').BaseOverlayFactory; 4 | 5 | module.exports = { 6 | componentDidMount: function () { 7 | var element = this.getDOMNode(); 8 | 9 | OverlayFactory.create({ 10 | point: this.props.point, 11 | el: element, 12 | map: this.props.map 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "idle-maps", 3 | "version": "1.0.1-alpha", 4 | "description": "React components for asynchronous google maps", 5 | "main": "dist/idle-maps.js", 6 | "scripts": { 7 | "test": "eslint index.js webpack.config.js test lib examples && mocha --compilers jsx:jsx-require-extension -r ./test/helpers/jsdom.js test", 8 | "build": "npm test && webpack index.js dist/idle-maps.js", 9 | "release": "npm run build && COMPRESS=true webpack index.js dist/idle-maps.min.js", 10 | "start": "npm run build && webpack examples/index.jsx examples/app.js && webpack-dev-server --no-info --content-base examples --hot --colors" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/zpratt/idle-maps.git" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "google-maps" 19 | ], 20 | "author": "zach pratt", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/zpratt/idle-maps/issues" 24 | }, 25 | "homepage": "https://github.com/zpratt/idle-maps", 26 | "devDependencies": { 27 | "chai": "^2.2.0", 28 | "eslint": "^0.17.1", 29 | "jsdom": "^4.0.5", 30 | "jsx-loader": "^0.12.2", 31 | "jsx-require-extension": "^0.2.0", 32 | "mocha": "^2.2.1", 33 | "react": "^0.13.0", 34 | "sinon": "^1.14.1", 35 | "webpack": "^1.7.2", 36 | "webpack-dev-server": "^1.7.0" 37 | }, 38 | "peerDependencies": { 39 | "react": "^0.13.0" 40 | }, 41 | "dependencies": { 42 | "async-google-maps": "^0.2.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/google-maps-mixin.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var GoogleMapsMixin = require('../lib/google-maps-mixin'), 4 | 5 | MapLoader = require('async-google-maps').MapLoader, 6 | 7 | React = require('react'), 8 | ReactTestUtils = require('react/lib/ReactTestUtils'), 9 | 10 | sinon = require('sinon'), 11 | 12 | expectedApiKey, 13 | expectedApiVersion, 14 | ComponentWithMixin, 15 | ElementWithMixin, 16 | sandbox; 17 | 18 | describe('Google Maps API Mixin Test Suite', function () { 19 | beforeEach(function () { 20 | sandbox = sinon.sandbox.create(); 21 | 22 | sandbox.stub(MapLoader, 'load'); 23 | 24 | ComponentWithMixin = React.createClass({ 25 | mixins: [GoogleMapsMixin], 26 | 27 | render: function () { 28 | return React.DOM.div(); 29 | } 30 | }); 31 | 32 | expectedApiKey = 'somekey'; 33 | expectedApiVersion = 'someversion'; 34 | 35 | ElementWithMixin = React.createElement(ComponentWithMixin, { 36 | apiKey: expectedApiKey, 37 | apiVersion: expectedApiVersion 38 | }); 39 | }); 40 | 41 | afterEach(function () { 42 | sandbox.restore(); 43 | }); 44 | 45 | it('should load the map with the provided api key and version', function () { 46 | sinon.assert.notCalled(MapLoader.load); 47 | 48 | ReactTestUtils.renderIntoDocument(ElementWithMixin); 49 | 50 | sinon.assert.calledOnce(MapLoader.load); 51 | sinon.assert.calledWith(MapLoader.load, { 52 | key: expectedApiKey, 53 | version: expectedApiVersion 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/helpers/jsdom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jsdom = require('jsdom'); 4 | 5 | /** 6 | * Borrowed from: https://github.com/tmpvar/jsdom/issues/135#issuecomment-68191941 7 | */ 8 | function applyJsdomWorkaround(window) { 9 | Object.defineProperties(window.HTMLElement.prototype, { 10 | offsetLeft: { 11 | get: function () { 12 | return parseFloat(window.getComputedStyle(this).marginLeft) || 0; 13 | } 14 | }, 15 | offsetTop: { 16 | get: function () { 17 | return parseFloat(window.getComputedStyle(this).marginTop) || 0; 18 | } 19 | }, 20 | offsetHeight: { 21 | get: function () { 22 | return parseFloat(window.getComputedStyle(this).height) || 0; 23 | } 24 | }, 25 | offsetWidth: { 26 | get: function () { 27 | return parseFloat(window.getComputedStyle(this).width) || 0; 28 | } 29 | } 30 | }); 31 | } 32 | 33 | function setupDom() { 34 | var baseMarkup = '', 35 | window = jsdom.jsdom(baseMarkup).defaultView; 36 | 37 | global.window = window; 38 | global.document = window.document; 39 | global.navigator = window.navigator; 40 | applyJsdomWorkaround(window); 41 | } 42 | 43 | setupDom(); 44 | -------------------------------------------------------------------------------- /test/map-instance-mixin.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MapMixin = require('../lib/map-instance-mixin.jsx'), 4 | 5 | MapLoader = require('async-google-maps').MapLoader, 6 | 7 | React = require('react'), 8 | ReactTestUtils = require('react/lib/ReactTestUtils'), 9 | 10 | sinon = require('sinon'), 11 | expect = require('chai').expect, 12 | 13 | MapComponent, 14 | MapElement, 15 | 16 | expectedMapOptions, 17 | mapIdleCallback, 18 | idleSpy, 19 | 20 | fakeMapInstance, 21 | sandbox; 22 | 23 | function createFixtures() { 24 | expectedMapOptions = { 25 | some: 'key' 26 | }; 27 | 28 | fakeMapInstance = { 29 | fake: 'map' 30 | }; 31 | } 32 | function setupStubs() { 33 | sandbox.stub(MapLoader, 'create') 34 | .returns({ 35 | then: function (callback) { 36 | mapIdleCallback = callback; 37 | } 38 | }); 39 | 40 | idleSpy = sandbox.spy(); 41 | } 42 | describe('Google Map Instance Mixin Test Suite', function () { 43 | beforeEach(function () { 44 | sandbox = sinon.sandbox.create(); 45 | 46 | setupStubs(); 47 | createFixtures(); 48 | 49 | MapComponent = React.createClass({ 50 | mixins: [MapMixin], 51 | 52 | idle: idleSpy 53 | }); 54 | 55 | MapElement = React.createElement(MapComponent, { 56 | mapOptions: expectedMapOptions 57 | }); 58 | }); 59 | 60 | afterEach(function () { 61 | sandbox.restore(); 62 | }); 63 | 64 | it('should create a container for the google map instance', function () { 65 | var renderedElement = ReactTestUtils.renderIntoDocument(MapElement), 66 | domNode = renderedElement.getDOMNode(); 67 | 68 | expect(domNode.className).to.equal('map-container'); 69 | }); 70 | 71 | it('should create a container for the google map instance', function () { 72 | var renderedElement; 73 | 74 | sinon.assert.notCalled(MapLoader.create); 75 | 76 | renderedElement = ReactTestUtils.renderIntoDocument(MapElement); 77 | 78 | sinon.assert.calledOnce(MapLoader.create); 79 | sinon.assert.calledWith(MapLoader.create, renderedElement.getDOMNode(), expectedMapOptions); 80 | }); 81 | 82 | it('should notify the component extended with the mixin when the map is idle', function () { 83 | sinon.assert.notCalled(idleSpy); 84 | 85 | ReactTestUtils.renderIntoDocument(MapElement); 86 | 87 | mapIdleCallback(fakeMapInstance); 88 | 89 | sinon.assert.calledOnce(idleSpy); 90 | sinon.assert.calledWith(idleSpy, fakeMapInstance); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/overlay-mixin.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var OverlayMixin = require('../lib/overlay-mixin'), 4 | BaseOverlayFactory = require('async-google-maps').BaseOverlayFactory, 5 | 6 | React = require('react'), 7 | ReactTestUtils = require('react/lib/ReactTestUtils'), 8 | 9 | sinon = require('sinon'), 10 | 11 | ComponentWithMixin, 12 | ElementWithMixin, 13 | 14 | fakeMap, 15 | fakeLatLngLiteral, 16 | sandbox; 17 | 18 | function createFixtures() { 19 | fakeMap = { 20 | fake: 'map' 21 | }; 22 | 23 | fakeLatLngLiteral = { 24 | fake: 'LatLng' 25 | }; 26 | } 27 | describe('Google Maps Overlay View Mixin Test Suite', function () { 28 | beforeEach(function () { 29 | sandbox = sinon.sandbox.create(); 30 | 31 | createFixtures(); 32 | 33 | sandbox.stub(BaseOverlayFactory, 'create'); 34 | 35 | ComponentWithMixin = React.createClass({ 36 | mixins: [OverlayMixin], 37 | 38 | render: function () { 39 | return React.DOM.div(); 40 | } 41 | }); 42 | 43 | ElementWithMixin = React.createElement(ComponentWithMixin, { 44 | map: fakeMap, 45 | point: fakeLatLngLiteral 46 | }); 47 | }); 48 | 49 | afterEach(function () { 50 | sandbox.restore(); 51 | }); 52 | 53 | it('should bind the DOM node of the component to the overlay', function () { 54 | var renderedComponent = ReactTestUtils.renderIntoDocument(ElementWithMixin), 55 | componentElement = renderedComponent.getDOMNode(); 56 | 57 | sinon.assert.calledOnce(BaseOverlayFactory.create); 58 | sinon.assert.calledWith(BaseOverlayFactory.create, { 59 | point: fakeLatLngLiteral, 60 | el: componentElement, 61 | map: fakeMap 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'), 2 | plugins = [ 3 | new webpack.DefinePlugin({ 4 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 5 | }) 6 | ]; 7 | 8 | if (process.env.COMPRESS) { 9 | plugins.push( 10 | new webpack.optimize.UglifyJsPlugin({ 11 | compressor: { 12 | warnings: false 13 | } 14 | }) 15 | ); 16 | } 17 | 18 | module.exports = { 19 | output: { 20 | library: 'IdleMaps', 21 | libraryTarget: 'umd' 22 | }, 23 | externals: [ 24 | { 25 | 'react': { 26 | root: 'React', 27 | commonjs2: 'react', 28 | commonjs: 'react', 29 | amd: 'react' 30 | } 31 | } 32 | ], 33 | module: { 34 | loaders: [ 35 | { test: /\.jsx$/, exclude: /node_modules/, loader: 'jsx-loader'} 36 | ] 37 | }, 38 | node: { 39 | Buffer: false 40 | }, 41 | plugins: plugins 42 | }; 43 | --------------------------------------------------------------------------------