├── .gitignore
├── lib
├── google-maps-shim.js
├── base-overlay-factory.js
├── base-overlay.js
└── map-loader.js
├── .npmignore
├── .travis.yml
├── conf.json
├── .editorconfig
├── test
├── google-maps-shim.spec.js
├── helpers
│ ├── fake-google-maps.js
│ ├── sync-promise.js
│ └── jsdom.js
├── base-overlay-factory.spec.js
├── base-overlay.spec.js
└── map-loader.spec.js
├── index.js
├── package.json
├── LICENSE
├── README.md
└── .eslintrc
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 | node_modules
4 |
--------------------------------------------------------------------------------
/lib/google-maps-shim.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = global.google;
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test
2 | .idea
3 | .travis.yml
4 | .editorconfig
5 | .eslintrc
6 | .gitignore
7 | conf.json
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "iojs"
4 | env:
5 | global:
6 | - NODE_ENV=travisci
7 |
--------------------------------------------------------------------------------
/conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "opts": {
3 | "destination": "./docs/",
4 | "recurse": true
5 | },
6 | "plugins": [
7 | "plugins/markdown"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/test/google-maps-shim.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var expect = require('chai').expect,
4 | fakeGoogleMapsApi = require('./helpers/fake-google-maps.js'),
5 | googleMaps = require('../lib/google-maps-shim');
6 |
7 | describe('Google Maps Global Shim Test Suite', function () {
8 | it('should shim the google global so that it can be `require`\'d', function () {
9 | expect(googleMaps).to.equal(fakeGoogleMapsApi);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * @author zach pratt
5 | * @licence MIT
6 | * @module async-google-maps
7 | */
8 |
9 | var MapLoader = require('./lib/map-loader'),
10 | BaseOverlayFactory = require('./lib/base-overlay-factory');
11 |
12 | module.exports = {
13 | /**
14 | * See {@link async-google-maps/map-loader}
15 | * */
16 | MapLoader: MapLoader,
17 | /**
18 | * See {@link async-google-maps/base-overlay-factory}
19 | * */
20 | BaseOverlayFactory: BaseOverlayFactory
21 | };
22 |
--------------------------------------------------------------------------------
/test/helpers/fake-google-maps.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function OverlayView() {
4 | return;
5 | }
6 |
7 | OverlayView.prototype.getPanes = function () {
8 | return;
9 | };
10 |
11 | OverlayView.prototype.getProjection = function () {
12 | return;
13 | };
14 |
15 | var fakeGoogleMapsApi = {
16 | maps: {
17 | Map: function () {
18 | return;
19 | },
20 | event: {
21 | addListenerOnce: function () {
22 | return;
23 | }
24 | },
25 | LatLng: function () {
26 |
27 | },
28 | OverlayView: OverlayView
29 | }
30 | };
31 |
32 | global.google = fakeGoogleMapsApi;
33 |
34 | module.exports = fakeGoogleMapsApi;
35 |
--------------------------------------------------------------------------------
/test/helpers/sync-promise.js:
--------------------------------------------------------------------------------
1 | var sandbox;
2 |
3 | function Promise(resolver) {
4 | var thenCallbacks = [],
5 | resolved;
6 |
7 | this.reject = sandbox.spy();
8 | this.resolve = sandbox.spy(function () {
9 | resolved = true;
10 | });
11 |
12 | this.then = function (callback) {
13 | thenCallbacks.push(callback);
14 |
15 | if (resolved) {
16 | callback();
17 | }
18 | };
19 |
20 | resolver(this.resolve, this.reject);
21 | }
22 |
23 | module.exports = {
24 | Promise: Promise,
25 | setSandbox: function (suppliedSandbox) {
26 | sandbox = suppliedSandbox;
27 | },
28 | unsetSandbox: function () {
29 | sandbox = null;
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "async-google-maps",
3 | "version": "0.2.3",
4 | "description": "Asynchronous loading and creation of google maps",
5 | "engines": {
6 | "node": ">=0.12"
7 | },
8 | "main": "index.js",
9 | "scripts": {
10 | "docs": "rm -rf docs && node_modules/.bin/jsdoc -P package.json -R README.md -c conf.json index.js lib/*.js",
11 | "test": "node_modules/.bin/eslint test/ lib/ && node_modules/.bin/mocha -r ./test/helpers/jsdom.js"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/zpratt/async-google-maps.git"
16 | },
17 | "keywords": [
18 | "google",
19 | "maps"
20 | ],
21 | "author": "zach pratt",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/zpratt/async-google-maps/issues"
25 | },
26 | "homepage": "https://github.com/zpratt/async-google-maps",
27 | "devDependencies": {
28 | "chai": "^2.1.1",
29 | "eslint": "^0.18.0",
30 | "jsdoc": "^3.3.0-beta3",
31 | "jsdom": "^5.0.0",
32 | "mocha": "^2.2.1",
33 | "proxyquire": "^1.4.0",
34 | "sinon": "^1.14.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/base-overlay-factory.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * @module async-google-maps/base-overlay-factory
5 | */
6 |
7 | var MapLoader = require('./map-loader');
8 |
9 | module.exports = {
10 | /**
11 | * Creates an instance of a custom overlay that will position itself on the map based on the overlay dimensions
12 | * and the provided point.
13 | *
14 | * @param options - Options passed to the base overlay constructor
15 | * @param options.el - The element that the overlay will use to append into the overlay pane.
16 | * @param options.point - The lat/lng that the overlay will use to position itself on the map.
17 | * @param options.cacheDimensions - Instruct the Overlay to cache its dimensions after the first time it is drawn
18 | */
19 | create: function (options) {
20 | MapLoader.loaded.then(function () {
21 | var BaseOverlay = require('./base-overlay'),
22 | overlayInstance = new BaseOverlay(options),
23 | shouldCacheDimensions = options.cacheDimensions;
24 |
25 | overlayInstance.setMap(options.map);
26 |
27 | if (shouldCacheDimensions) {
28 | overlayInstance.cacheDimensions();
29 | }
30 | });
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/base-overlay.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var google = global.google;
4 |
5 | function getOffsetHeightAndWidth() {
6 | return {
7 | height: this.el.offsetHeight,
8 | width: this.el.offsetWidth
9 | };
10 | }
11 | function getDimensions() {
12 | if (this.useCache) {
13 | if (!this.dimensions) {
14 | this.dimensions = getOffsetHeightAndWidth.call(this);
15 | }
16 |
17 | return this.dimensions;
18 | }
19 |
20 | return getOffsetHeightAndWidth.call(this);
21 | }
22 |
23 | function positionOverlayByDimensions(projectedLatLng) {
24 | var dimensions = getDimensions.call(this),
25 | offsetHeight = dimensions.height,
26 | offsetWidth = dimensions.width;
27 |
28 | this.el.style.top = projectedLatLng.y - offsetHeight + 'px';
29 | this.el.style.left = projectedLatLng.x - Math.floor(offsetWidth / 2) + 'px';
30 | }
31 |
32 | function draw() {
33 | var projection = this.getProjection(),
34 | projectedLatLng = projection.fromLatLngToDivPixel(this.point);
35 |
36 | positionOverlayByDimensions.call(this, projectedLatLng);
37 | }
38 |
39 | function onRemove() {
40 | var parentEl = this.el.parentNode;
41 |
42 | parentEl.removeChild(this.el);
43 | }
44 |
45 | function onAdd() {
46 | var panes = this.getPanes();
47 |
48 | panes.overlayLayer.appendChild(this.el);
49 | }
50 |
51 | function cacheDimensions() {
52 | this.useCache = true;
53 | }
54 |
55 | function BaseOverlay(options) {
56 | var point = options.point;
57 |
58 | this.el = options.el;
59 | this.point = new google.maps.LatLng(point.lat, point.lng);
60 |
61 | this.el.style.position = 'absolute';
62 | }
63 |
64 | BaseOverlay.prototype = Object.create(google.maps.OverlayView.prototype);
65 | BaseOverlay.prototype.constructor = BaseOverlay;
66 |
67 | BaseOverlay.prototype.onAdd = onAdd;
68 | BaseOverlay.prototype.onRemove = onRemove;
69 | BaseOverlay.prototype.draw = draw;
70 | BaseOverlay.prototype.cacheDimensions = cacheDimensions;
71 |
72 | module.exports = BaseOverlay;
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Asynchronous loading and creation of google maps
2 |
3 | [](https://travis-ci.org/zpratt/async-google-maps)
4 | [](https://www.npmjs.com/package/async-google-maps)
5 |
6 | ## Why use this module?
7 |
8 | I wrote this module to have the simplest possible public API and to handle the common case of waiting to add things to the map until it is `idle`. It is also targeted at making the creation of multiple map instances easier to manage, since each call to `create` will return a promise. There is [another library](https://github.com/sakren/node-google-maps) you can use if you don't like mine.
9 |
10 | Additionally, I have focused on code readability, test quality, and attempted to make this an easy module to TDD with as a dependency. You can easily stub the API, instead of having to verify properties of the module itself. TDD is a passion of mine.
11 |
12 | ### Not fully baked yet
13 |
14 | I have yet to include the options for the business version, but I expect to include that in the next release.
15 |
16 | ## Running tests
17 |
18 | This module is 100% test-driven. Please feel free to run the tests and critique them.
19 |
20 | 1. Install io.js (needed for jsdom)
21 | 2. `git clone` ...
22 | 3. `npm i`
23 | 4. `npm test`
24 |
25 | ## Usage
26 |
27 | This is a pure commonjs module with no production dependencies. I recommend using either [webpack](http://webpack.github.io/docs/) or [browserify](https://github.com/substack/node-browserify) to build.
28 |
29 | See the [wiki](https://github.com/zpratt/async-google-maps/wiki) for usage examples.
30 |
31 | ## Documentation
32 |
33 | API documentation can be found [here](http://zpratt.github.io/async-google-maps/docs/async-google-maps/0.2.2/index.html).
34 |
35 | ### Promises
36 |
37 | This library leverages the ES6 promise implementation, which is currently [available in most modern browsers](http://caniuse.com/#feat=promises). If you wish to use it with browsers that do not support the ES6 promise implementation, then I recommend using [a polyfill](https://github.com/jakearchibald/es6-promise).
38 |
39 | ### Why?
40 |
41 | I have intentionally limited the production dependencies of this module, so you are not forced to use a particular library.
42 |
--------------------------------------------------------------------------------
/test/base-overlay-factory.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var BaseOverlayFactory,
4 |
5 | proxyquire = require('proxyquire'),
6 | sinon = require('sinon'),
7 |
8 | fakeMap,
9 | fakeOptions,
10 |
11 | mapLoadedSpy,
12 | BaseOverlaySpy,
13 | sandbox;
14 |
15 | function resolveMapPromiseAndGetOverlayInstance() {
16 | mapLoadedSpy.firstCall.yield();
17 |
18 | return BaseOverlaySpy.returnValues[0];
19 | }
20 | describe('Base Overlay Factory Test Suite', function () {
21 | beforeEach(function () {
22 | sandbox = sinon.sandbox.create();
23 |
24 | fakeMap = {};
25 | fakeOptions = {
26 | map: fakeMap
27 | };
28 |
29 | mapLoadedSpy = sandbox.spy();
30 | BaseOverlaySpy = sandbox.spy(function () {
31 | return {
32 | setMap: sandbox.spy(),
33 | cacheDimensions: sandbox.spy()
34 | };
35 | });
36 |
37 | BaseOverlayFactory = proxyquire('../lib/base-overlay-factory', {
38 | './base-overlay': BaseOverlaySpy,
39 | './map-loader': {
40 | loaded: {
41 | then: mapLoadedSpy
42 | }
43 | }
44 | });
45 | });
46 |
47 | afterEach(function () {
48 | sandbox.restore();
49 | });
50 |
51 | it('should create an instance of the overlay after the google maps API has loaded', function () {
52 | BaseOverlayFactory.create(fakeOptions);
53 | sinon.assert.notCalled(BaseOverlaySpy);
54 |
55 | mapLoadedSpy.firstCall.yield();
56 |
57 | sinon.assert.calledOnce(BaseOverlaySpy);
58 | sinon.assert.calledWith(BaseOverlaySpy, fakeOptions);
59 | sinon.assert.calledWithNew(BaseOverlaySpy);
60 | });
61 |
62 | it('should set the map on the overlay instance after creating it', function () {
63 | var fakeOverlayInstance;
64 |
65 | BaseOverlayFactory.create(fakeOptions);
66 | fakeOverlayInstance = resolveMapPromiseAndGetOverlayInstance();
67 |
68 | sinon.assert.calledOnce(fakeOverlayInstance.setMap);
69 | sinon.assert.calledWith(fakeOverlayInstance.setMap, fakeMap);
70 | });
71 |
72 | it('should tell the overlay instance to cache its dimensions when the option is set', function () {
73 | var fakeOverlayInstance;
74 |
75 | fakeOptions.cacheDimensions = true;
76 |
77 | BaseOverlayFactory.create(fakeOptions);
78 | fakeOverlayInstance = resolveMapPromiseAndGetOverlayInstance();
79 | sinon.assert.calledOnce(fakeOverlayInstance.cacheDimensions);
80 | });
81 |
82 | it('should not tell the overlay instance to cache its dimensions when the option is not set', function () {
83 | var fakeOverlayInstance;
84 |
85 | BaseOverlayFactory.create(fakeOptions);
86 | fakeOverlayInstance = resolveMapPromiseAndGetOverlayInstance();
87 | sinon.assert.notCalled(fakeOverlayInstance.cacheDimensions);
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/.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 | "space-in-parens": [2, "never"],
91 | "strict": 2,
92 | "unnecessary-strict": 0,
93 | "use-isnan": 2,
94 | "valid-typeof": 0,
95 | "wrap-iife": 2,
96 | "wrap-regex": 0,
97 | "yoda": "always"
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/lib/map-loader.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * @module async-google-maps/map-loader
5 | */
6 |
7 | var URL_PREFIX = '//maps.googleapis.com/maps/api/js',
8 | CALLBACK_IDENTIFIER = 'mapLoaded',
9 |
10 | scriptLoaded = false,
11 | mapLoadedDeferred,
12 | mapLoadedPromise;
13 |
14 | global[CALLBACK_IDENTIFIER] = function mapLoaded() {
15 | mapLoadedDeferred.resolve();
16 | };
17 |
18 | function buildUrl(options) {
19 | var mapsUrl = URL_PREFIX
20 | + '?v=' + options.version;
21 |
22 | if (options.client && options.channel) {
23 | mapsUrl += '&client=' + options.client
24 | + '&channel=' + options.channel;
25 | } else {
26 | mapsUrl += '&key=' + options.key;
27 | }
28 |
29 | mapsUrl += '&callback=' + CALLBACK_IDENTIFIER;
30 |
31 | return mapsUrl;
32 | }
33 |
34 | function createDeferredToAllowForResolutionAfterGoogleMapsLoaderFinishes(resolve, reject) {
35 | mapLoadedDeferred = {
36 | resolve: resolve,
37 | reject: reject
38 | };
39 | }
40 |
41 | function createMapLoadedPromise() {
42 | mapLoadedPromise = new Promise(function (resolve, reject) {
43 | createDeferredToAllowForResolutionAfterGoogleMapsLoaderFinishes(resolve, reject);
44 | });
45 | }
46 |
47 | function appendScript(scriptEl) {
48 | document.head.appendChild(scriptEl);
49 | scriptLoaded = true;
50 | }
51 |
52 | function appendScriptWhenDocumentReady(scriptEl) {
53 | if (!scriptLoaded && document.readyState !== 'loading') {
54 | appendScript(scriptEl);
55 | } else {
56 | document.addEventListener('DOMContentLoaded', function () {
57 | appendScript(scriptEl);
58 | });
59 | }
60 | }
61 |
62 | function injectGoogleMapsScript(options) {
63 | var scriptEl = document.createElement('script');
64 | scriptEl.src = buildUrl(options);
65 | scriptEl.type = 'text/javascript';
66 |
67 | appendScriptWhenDocumentReady(scriptEl);
68 | }
69 |
70 | function init() {
71 | createMapLoadedPromise();
72 | }
73 |
74 | init();
75 |
76 | module.exports = {
77 | /**
78 | * Asynchronously loads the google maps javascript library, given the supplied options.
79 | * Returns a promise that will be resolved once the google maps loader has finished.
80 | * Once the promise resolves, it is safe to reference anything under the `google.maps` namespace.
81 | * This method should only be called once for a given application.
82 | *
83 | * @param {Object} options - The configuration used to load the google maps API
84 | * @param {string} options.version - The version of the google maps API to load
85 | * @param {string} options.key - The API key of the consuming application
86 | * @returns {Promise}
87 | * */
88 | load: function loadGoogleMapsAsync(options) {
89 | injectGoogleMapsScript(options);
90 |
91 | return mapLoadedPromise;
92 | },
93 |
94 | /**
95 | * @property {Promise} loaded - A reference to a promise that is resolved once the google maps API is ready.
96 | */
97 | loaded: (function () {
98 | return mapLoadedPromise;
99 | }()),
100 |
101 | /**
102 | * Creates a map instance given the supplied options. The options will be passed into the `google.maps.Map` constructor,
103 | * therefore, all options from the [google maps api](https://developers.google.com/maps/documentation/javascript/reference#MapOptions) can be used.
104 | * This function returns a promise which will be resolved once the newly created map instance is in the `idle` state,
105 | * which is the point at which overlays, markers, and geometries can be added to the map.
106 | *
107 | * @param mapContainer - The element to attach the map instance to
108 | * @param options - Options passed to the google Map constructor
109 | * @returns {Promise}
110 | * */
111 | create: function createMap(mapContainer, options) {
112 | return new Promise(function mapIdleResolver(resolve) {
113 | mapLoadedPromise.then(function whenMapHasLoaded() {
114 | var googleMaps = require('./google-maps-shim.js'),
115 | mapInstance = new googleMaps.maps.Map(mapContainer, options);
116 |
117 | googleMaps.maps.event.addListenerOnce(mapInstance, 'idle', function mapIdleHandler() {
118 | resolve(mapInstance);
119 | });
120 | });
121 | });
122 | }
123 | };
124 |
--------------------------------------------------------------------------------
/test/base-overlay.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fakeGoogleMaps = require('./helpers/fake-google-maps'),
4 | BaseOverlay = require('../lib/base-overlay'),
5 |
6 | expect = require('chai').expect,
7 | sinon = require('sinon'),
8 |
9 | sandbox,
10 |
11 | fakeMapInstance,
12 | latLngLiteral,
13 | fakeLatLng,
14 | projectedLatLng,
15 |
16 | initialOffsetHeight,
17 | initialOffsetWidth,
18 |
19 | overlayPaneElement,
20 | overlayElement,
21 | overlayOptions,
22 | overlayInstance;
23 |
24 | function setDimensionsOfOverlayElement() {
25 | overlayElement.style.height = initialOffsetHeight + 'px';
26 | overlayElement.style.width = initialOffsetWidth + 'px';
27 | }
28 |
29 | function givenTheOverlayIsAddedToTheDom() {
30 | overlayInstance.onAdd();
31 | setDimensionsOfOverlayElement();
32 | }
33 |
34 | function calculateTop() {
35 | return projectedLatLng.y - initialOffsetHeight + 'px';
36 | }
37 |
38 | function calculateLeft() {
39 | return projectedLatLng.x - Math.floor(initialOffsetWidth / 2) + 'px';
40 | }
41 |
42 | function randomInt(min, max) {
43 | return Math.floor(Math.random() * (max - min)) + min;
44 | }
45 |
46 | function generateHeightAndWidth() {
47 | initialOffsetHeight = randomInt(20, 1000);
48 | initialOffsetWidth = randomInt(20, 1000);
49 | }
50 |
51 | function setFakeData() {
52 | generateHeightAndWidth();
53 |
54 | latLngLiteral = {
55 | lat: 0.0,
56 | lng: 0.0
57 | };
58 |
59 | fakeLatLng = {};
60 | fakeMapInstance = {};
61 |
62 | projectedLatLng = {
63 | x: initialOffsetWidth + randomInt(1, 100),
64 | y: initialOffsetHeight + randomInt(1, 100)
65 | };
66 | }
67 |
68 | function setupDom() {
69 | overlayPaneElement = document.createElement('div');
70 | overlayElement = document.createElement('div');
71 | }
72 |
73 | function setupStubs() {
74 | sandbox.stub(fakeGoogleMaps.maps.OverlayView.prototype, 'getPanes')
75 | .returns({
76 | overlayLayer: overlayPaneElement
77 | });
78 |
79 | sandbox.stub(fakeGoogleMaps.maps.OverlayView.prototype, 'getProjection')
80 | .returns({
81 | fromLatLngToDivPixel: function (latLng) {
82 | if (latLng === fakeLatLng) {
83 | return projectedLatLng;
84 | }
85 | }
86 | });
87 |
88 | sandbox.stub(fakeGoogleMaps.maps, 'LatLng', function (lat, lng) {
89 | if (lat === latLngLiteral.lat && lng === latLngLiteral.lng) {
90 | return fakeLatLng;
91 | }
92 | });
93 | }
94 |
95 | describe('Base Overlay Test Suite', function () {
96 | beforeEach(function () {
97 | sandbox = sinon.sandbox.create();
98 |
99 | setFakeData();
100 | setupDom();
101 | setupStubs();
102 |
103 | overlayOptions = {
104 | point: latLngLiteral,
105 | el: overlayElement,
106 | map: fakeMapInstance
107 | };
108 |
109 | overlayInstance = new BaseOverlay(overlayOptions);
110 | });
111 |
112 | afterEach(function () {
113 | sandbox.restore();
114 | });
115 |
116 | it('should be an instance of a google maps overlay view', function () {
117 | expect(overlayInstance).to.be.an.instanceOf(fakeGoogleMaps.maps.OverlayView);
118 | });
119 |
120 | it('should absolutely position the overlay', function () {
121 | expect(overlayElement.style.position).to.equal('absolute');
122 | });
123 |
124 | it('should append the provided element into the DOM', function () {
125 | expect(overlayPaneElement.contains(overlayElement)).to.equal(false);
126 |
127 | overlayInstance.onAdd();
128 |
129 | expect(overlayPaneElement.contains(overlayElement)).to.equal(true);
130 | });
131 |
132 | it('should position the overlay once the map is idle', function () {
133 | var expectedTop = calculateTop(),
134 | expectedLeft = calculateLeft();
135 |
136 | expect(overlayElement.style.top).to.equal('');
137 | expect(overlayElement.style.left).to.equal('');
138 |
139 | givenTheOverlayIsAddedToTheDom();
140 |
141 | overlayInstance.draw();
142 |
143 | expect(overlayElement.style.top).to.equal(expectedTop);
144 | expect(overlayElement.style.left).to.equal(expectedLeft);
145 | });
146 |
147 | it('should remove itself from the DOM when its map is unset', function () {
148 | overlayInstance.onAdd();
149 |
150 | overlayInstance.onRemove();
151 |
152 | expect(overlayPaneElement.contains(overlayElement)).to.equal(false);
153 | });
154 |
155 | it('should cache the dimensions of the associated DOM element', function () {
156 | var expectedTop = calculateTop(),
157 | expectedLeft = calculateLeft();
158 |
159 | overlayInstance.cacheDimensions();
160 |
161 | givenTheOverlayIsAddedToTheDom();
162 | overlayInstance.draw();
163 |
164 | generateHeightAndWidth();
165 | setDimensionsOfOverlayElement();
166 |
167 | overlayInstance.draw();
168 | expect(overlayElement.style.top).to.equal(expectedTop);
169 | expect(overlayElement.style.left).to.equal(expectedLeft);
170 | });
171 |
172 | it('should not cache the dimensions until after the overlay has been added to the DOM and drawn', function () {
173 | var expectedTop = calculateTop(),
174 | expectedLeft = calculateLeft();
175 |
176 | overlayInstance.cacheDimensions();
177 | overlayInstance.draw();
178 |
179 | expect(overlayElement.style.top).to.not.equal(expectedTop);
180 | expect(overlayElement.style.left).to.not.equal(expectedLeft);
181 | });
182 | });
183 |
--------------------------------------------------------------------------------
/test/map-loader.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fakeGoogle = require('./helpers/fake-google-maps.js'),
4 | MapLoader,
5 |
6 | SyncPromise = require('./helpers/sync-promise.js'),
7 | path = require('path'),
8 |
9 | expect = require('chai').expect,
10 | sinon = require('sinon'),
11 |
12 | EXPECTED_GLOBAL_CALLBACK = 'mapLoaded',
13 | EXPECTED_KEY_URL_TEMPLATE = '//maps.googleapis.com/maps/api/js?v={VERSION}&key={KEY}&callback={CALLBACK}',
14 | EXPECTED_CLIENT_CHANNEL_URL_TEMPLATE = '//maps.googleapis.com/maps/api/js?v={VERSION}&client={CLIENT}&channel={CHANNEL}&callback={CALLBACK}',
15 |
16 | sandbox,
17 |
18 | fakeMapInstance,
19 |
20 | expectedVersion,
21 | expectedKey,
22 | expectedClient,
23 | expectedChannel,
24 | expectedKeyUrl,
25 | expectedClientChannelUrl,
26 | loadOptions,
27 | loadClientChannelOptions,
28 | fakeMapOptions,
29 | mapContainer,
30 |
31 | documentReadyCallback,
32 | mapIdleCallback;
33 |
34 | function triggerDocumentReady() {
35 | documentReadyCallback();
36 | }
37 |
38 | function givenGoogleMapsLibHasLoaded() {
39 | MapLoader.load(loadOptions);
40 |
41 | global[EXPECTED_GLOBAL_CALLBACK]();
42 | }
43 |
44 | function defineStubs() {
45 | sandbox.stub(global, 'Promise', function (resolver) {
46 | return new SyncPromise.Promise(resolver);
47 | });
48 |
49 | sandbox.stub(fakeGoogle.maps, 'Map')
50 | .returns(fakeMapInstance);
51 |
52 | sandbox.stub(fakeGoogle.maps.event, 'addListenerOnce', function (mapInstance, eventName, callback) {
53 | if (mapInstance === fakeMapInstance && eventName === 'idle') {
54 | mapIdleCallback = callback;
55 | }
56 | });
57 |
58 | sandbox.stub(document, 'addEventListener', function (event, callback) {
59 | if (event === 'DOMContentLoaded') {
60 | documentReadyCallback = callback;
61 | }
62 | });
63 | }
64 |
65 | function defineFakeData() {
66 | fakeMapInstance = {};
67 | fakeMapOptions = {};
68 | mapContainer = {};
69 |
70 | expectedVersion = '3.20';
71 | expectedKey = 'somekey';
72 | expectedClient = 'myclient';
73 | expectedChannel = 'mychannel';
74 |
75 | expectedKeyUrl = EXPECTED_KEY_URL_TEMPLATE
76 | .replace('{VERSION}', expectedVersion)
77 | .replace('{KEY}', expectedKey)
78 | .replace('{CALLBACK}', EXPECTED_GLOBAL_CALLBACK);
79 |
80 | expectedClientChannelUrl = EXPECTED_CLIENT_CHANNEL_URL_TEMPLATE
81 | .replace('{VERSION}', expectedVersion)
82 | .replace('{CLIENT}', expectedClient)
83 | .replace('{CHANNEL}', expectedChannel)
84 | .replace('{CALLBACK}', EXPECTED_GLOBAL_CALLBACK);
85 |
86 | loadOptions = {
87 | version: expectedVersion,
88 | key: expectedKey
89 | };
90 |
91 | loadClientChannelOptions = {
92 | version: expectedVersion,
93 | client: expectedClient,
94 | channel: expectedChannel
95 | };
96 | }
97 |
98 | describe('Google Maps Loader Test Suite', function () {
99 | beforeEach(function () {
100 | sandbox = sinon.sandbox.create();
101 | SyncPromise.setSandbox(sandbox);
102 |
103 | defineFakeData();
104 | defineStubs();
105 | });
106 |
107 | afterEach(function () {
108 | var cachedModule = path.join(__dirname, '..', 'lib', 'map-loader.js');
109 |
110 | sandbox.restore();
111 |
112 | SyncPromise.unsetSandbox();
113 | mapIdleCallback = null;
114 | documentReadyCallback = null;
115 | document.head.innerHTML = '';
116 |
117 | delete require.cache[cachedModule];
118 | });
119 |
120 | describe('When loaded before DOMContentLoaded event has fired', function () {
121 | beforeEach(function () {
122 | document.readyState = 'loading';
123 |
124 | MapLoader = require('../lib/map-loader');
125 | });
126 |
127 | it('should add the google maps key url script to the dom by default', function () {
128 | var scriptEl;
129 |
130 | MapLoader.load(loadOptions);
131 | expect(document.head.querySelector('script')).to.equal(null);
132 |
133 | triggerDocumentReady();
134 |
135 | scriptEl = document.head.querySelector('script');
136 | expect(scriptEl.src).to.equal(expectedKeyUrl);
137 | expect(scriptEl.type).to.equal('text/javascript');
138 | });
139 |
140 | it('should add the google maps client/channel url script to the dom if client/channel found', function () {
141 | var scriptEl;
142 |
143 | MapLoader.load(loadClientChannelOptions);
144 | expect(document.head.querySelector('script')).to.equal(null);
145 |
146 | triggerDocumentReady();
147 |
148 | scriptEl = document.head.querySelector('script');
149 | expect(scriptEl.src).to.equal(expectedClientChannelUrl);
150 | expect(scriptEl.type).to.equal('text/javascript');
151 | });
152 |
153 | it('should resolve the promise once google maps is loaded and the global callback is invoked', function () {
154 | var mapLoadPromise = MapLoader.load(loadOptions);
155 |
156 | sinon.assert.notCalled(mapLoadPromise.resolve);
157 | global[EXPECTED_GLOBAL_CALLBACK]();
158 |
159 | sinon.assert.calledOnce(mapLoadPromise.resolve);
160 | });
161 |
162 | it('should resolve the promise once google maps is loaded and the global callback is invoked', function () {
163 | var mapLoadPromise = MapLoader.load(loadOptions);
164 |
165 | sinon.assert.notCalled(mapLoadPromise.resolve);
166 | global[EXPECTED_GLOBAL_CALLBACK]();
167 |
168 | sinon.assert.calledOnce(mapLoadPromise.resolve);
169 | });
170 |
171 | it('should create a map instance with the supplied options', function () {
172 | sinon.assert.notCalled(fakeGoogle.maps.Map);
173 | givenGoogleMapsLibHasLoaded();
174 |
175 | MapLoader.create(mapContainer, fakeMapOptions);
176 |
177 | sinon.assert.calledOnce(fakeGoogle.maps.Map);
178 | sinon.assert.calledWith(fakeGoogle.maps.Map, mapContainer, fakeMapOptions);
179 | sinon.assert.calledWithNew(fakeGoogle.maps.Map);
180 | });
181 |
182 | it('should resolve with a google maps instance once the map is idle', function () {
183 | var mapIdlePromise;
184 |
185 | givenGoogleMapsLibHasLoaded();
186 | mapIdlePromise = MapLoader.create(fakeMapOptions);
187 |
188 | sinon.assert.notCalled(mapIdlePromise.resolve);
189 |
190 | mapIdleCallback();
191 |
192 | sinon.assert.calledOnce(mapIdlePromise.resolve);
193 | sinon.assert.calledWith(mapIdlePromise.resolve, fakeMapInstance);
194 | });
195 |
196 | it('should expose a hook to know when the google maps API is ready', function () {
197 | var mapLoadedPromise = MapLoader.loaded,
198 | promiseFromLoad = MapLoader.load(loadOptions);
199 |
200 | expect(mapLoadedPromise).to.equal(promiseFromLoad);
201 | });
202 |
203 | it('should only load the google maps API once', function () {
204 | sinon.assert.calledOnce(global.Promise);
205 | global.Promise.reset();
206 |
207 | MapLoader.load(loadOptions);
208 | triggerDocumentReady();
209 | MapLoader.load(loadOptions);
210 |
211 | sinon.assert.notCalled(global.Promise);
212 | expect(document.getElementsByTagName('script')).to.have.length(1);
213 | });
214 | });
215 |
216 | describe('When loaded after DOMContentLoaded event has fired', function () {
217 | beforeEach(function () {
218 | document.readyState = 'interactive';
219 |
220 | MapLoader = require('../lib/map-loader');
221 | });
222 |
223 | it('should add the google maps script to the dom immediately', function () {
224 | var scriptEl;
225 |
226 | expect(document.head.querySelector('script')).to.equal(null);
227 | MapLoader.load(loadOptions);
228 |
229 | scriptEl = document.head.querySelector('script');
230 | expect(scriptEl.src).to.equal(expectedKeyUrl);
231 | expect(scriptEl.type).to.equal('text/javascript');
232 | });
233 |
234 | it('should only load the google maps API once', function () {
235 | sinon.assert.calledOnce(global.Promise);
236 | global.Promise.reset();
237 |
238 | MapLoader.load(loadOptions);
239 | MapLoader.load(loadOptions);
240 |
241 | sinon.assert.notCalled(global.Promise);
242 | expect(document.getElementsByTagName('script')).to.have.length(1);
243 | });
244 | });
245 | });
246 |
--------------------------------------------------------------------------------