Repository: yagoferrer/map-tools (js)

Issue:
Feature: addTemplate() to create custom controls 
```javascript
{
templateURL: "mytemplate.html", //template; "<div>MapCtrl</div>"
logic:"mycontroller.js",
css: 'style.css'
events: {
'.mydiv click': function(e) {}
 
}
 }
```




https://developers.google.com/maps/documentation/javascript/examples/places-searchbox

Pull request:
Gert/controls fix #133
This feature fix the #133.

Patch:
diff --git a/examples/add.control.html b/examples/add.control.html
new file mode 100644
index 0000000..39a8c65
--- /dev/null
+++ b/examples/add.control.html
@@ -0,0 +1,69 @@
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Document</title>
+</head>
+<style>
+    html, body, #mymap {
+        height: 100%;
+        margin: 0px;
+        padding: 0px;
+    }
+
+    li.active {
+        background: rgba(0,0,0,.08) !important;
+    }
+</style>
+<body>
+<script src="../dist/map-tools.js"></script>
+<script>
+    var map = new GMP({
+        id: 'mymap',
+        lat: 41.3833,
+        lng: 2.1833
+    }, function (err, instance) {
+        if (!err) {
+            addControl();
+        }
+    });
+
+    function addControl() {
+
+        map.addControl({
+            template:   '<div>' +
+                        '<button id="clickMe" class="test-button">ctrl 1</button>' +
+                        '<button class="test-button">ctrl 2</button>' +
+                        '</div>',
+            position:'top center',
+            style: {
+                'background-color': '#fff',
+                'border': '2px solid #fff',
+                'border-radius': '3px',
+                'box-shadow': '0 2px 6px rgba(0,0,0,.3)',
+                'cursor': 'pointer',
+                'marginBottom': '22px',
+                'text-align': 'center'
+            },
+            events: {
+                '.test-button click' : function (e) {
+                    alert('Clicked on custom control');
+                }
+            }
+        });
+
+        map.addControl({
+            templateURL: 'templates/custom.panel.html',
+            position:'right bottom',
+            events: {
+                '.menu li click' : function (e) {
+                    console.log('clicked on ', e.target);
+                    e.target.classList.toggle('active');
+                    }
+            }
+        });
+    }
+</script>
+<div id="mymap"></div>
+</body>
+</html>
diff --git a/examples/index.html b/examples/index.html
index 7177426..a60d581 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -16,5 +16,6 @@
 <h4><a href="examples/crossfilter.html">Data querying with Crossfilter</a></h4>
 <h4><a href="examples/geojson.html">GeoJSON</a></h4>
 <h4><a href="examples/topojson.html">TopoJSON</a></h4>
+<h4><a href="examples/add.control.html">Controls</a></h4>
 </body>
 </html>
diff --git a/examples/templates/custom.panel.html b/examples/templates/custom.panel.html
new file mode 100644
index 0000000..b8d58e4
--- /dev/null
+++ b/examples/templates/custom.panel.html
@@ -0,0 +1,18 @@
+<div class="main-panel" style="width: 100px; height: 85px; text-align: left;padding: 0 15px; background-color: #fff;">
+    <h4 style="margin: 5px 0;">Locations</h4>
+    <div style="overflow: scroll;height: 60px;">
+        <ul class="menu" style="list-style-type: none; margin: 0; padding: 0">
+            <li >Pubs</li>
+            <li >Clubs</li>
+            <li >Restaurants</li>
+            <li >Businesses</li>
+            <li >Hotels</li>
+            <li >Motels</li>
+            <li >Beauty & Spas</li>
+            <li >Breakfast & Brunch</li>
+            <li >Shopping</li>
+            <li >Nightlife</li>
+        </ul>
+    </div>
+
+</div>
\ No newline at end of file
diff --git a/lib/map-tools/addControl.js b/lib/map-tools/addControl.js
new file mode 100644
index 0000000..8f0ccec
--- /dev/null
+++ b/lib/map-tools/addControl.js
@@ -0,0 +1,89 @@
+/*jslint node: true */
+"use strict"
+
+var utils = require('map-tools/utils');
+var config = require('map-tools/config');
+
+module.exports = function(global, that) {
+
+	function getPositionKey(pos) {
+		return pos.toUpperCase().match(/\S+/g).join('_');
+	}
+
+	function hy2cmml(k) {
+		return k.replace(/-(.)/g, function(m, g) {
+			return g.toUpperCase();
+		});
+	}
+
+	function HTMLParser(aHTMLString){
+		var container = window.document.createElement('div');
+
+		container.innerHTML = aHTMLString;
+
+		return container;
+	}
+
+	function addPanel(options) {
+
+		var position,
+			panel,
+			e,
+			rule;
+
+		// default position
+		options.position = options.position || 'TOP_LEFT';
+
+		if (options.templateURL) {
+			var request = new XMLHttpRequest();
+			request.open("GET", options.templateURL, false);
+			request.send(null);
+			panel = HTMLParser(request.responseText);
+
+		}
+
+		// template options
+		if (typeof options.template === 'string') {
+			panel = HTMLParser(options.template);
+		}
+
+		// positioning options
+		if (options.position) {
+			// convert to google ControlPosition map position keys
+			options.position = getPositionKey(options.position);
+			position = google.maps.ControlPosition[options.position];
+		}
+
+		// style options
+		if (typeof options.style === 'object') {
+			for (rule in options.style) {
+				var ruleKey = hy2cmml(rule);
+				panel.style[ruleKey] = options.style[rule];
+			}
+		}
+
+		// event handler
+		if (options.events) {
+
+			for (e in options.events) {
+				var keys = e.match(/\S+/g);
+
+
+				var	event = keys.splice(-1); //event type
+				var selector = keys.join(' '); // selector string
+				var	elements = panel.querySelectorAll(selector);
+
+				[].forEach.call(elements, function(elm) {
+					google.maps.event.addDomListener( elm, event, options.events[e] );
+				});
+
+			}
+		}
+
+
+		that.instance.controls[position].push(panel);
+		return panel;
+	}
+
+	return addPanel;
+};
diff --git a/lib/map-tools/index.js b/lib/map-tools/index.js
index edf4fb8..48f95d8 100644
--- a/lib/map-tools/index.js
+++ b/lib/map-tools/index.js
@@ -14,6 +14,7 @@ module.exports = function (global) {
     this.addTopoJson = require('map-tools/topojson')(global, that).addTopoJson;
     this.addGeoJson = require('map-tools/topojson')(global, that).addGeoJson;
     this.updateFeature = require('map-tools/updateFeature')(global, that);
+    this.addControl = require('map-tools/addControl')(global, that);
     this.updateMarker = require('map-tools/updateMarker')(global, that);
     this.addGroup = require('map-tools/groups')(global, that).addGroup;
     this.updateGroup = require('map-tools/groups')(global, that).updateGroup;


Test patch:
diff --git a/test/bootstrap.js b/test/bootstrap.js
index 0e50670..9c1dcc8 100644
--- a/test/bootstrap.js
+++ b/test/bootstrap.js
@@ -17,5 +17,4 @@ global.GMP = window.GMP;
 
 // To test independent modules.
 global.window = window;
-
-
+global.google = window.google;
diff --git a/test/functional/addControl.Spec.js b/test/functional/addControl.Spec.js
new file mode 100644
index 0000000..02928c5
--- /dev/null
+++ b/test/functional/addControl.Spec.js
@@ -0,0 +1,47 @@
+describe('when calling addControl()', function () {
+	"use strict";
+
+	it('should add a single panel on the specified position', function () {
+		var map = new GMP({async: false, id: 'mymap', lat: 41.3833, lng: 2.1833});
+
+		var result = map.addControl({
+			template: '<div>' +
+			'<button id="clickMe" class="test-button">ctrl 1</button></div>',
+			position: 'top center'
+		});
+
+		expect(Object.keys(GMP.maps.mymap.instance.controls[2]).length).to.equal(1);
+
+	});
+
+	it('should add a single panel with the specified style options', function () {
+		var map = new GMP({async: false, id: 'mymap', lat: 41.3833, lng: 2.1833});
+
+		var panel = map.addControl({
+			template: '<div><button id="clickMe">ctrl 1</button></div>',
+			style: {
+				'background-color': '#fff',
+				'margin-bottom': '22px'
+			}
+
+		});
+
+		expect(panel.style).to.eql({backgroundColor: '#fff', marginBottom: '22px'})
+
+	});
+
+	it('should add a single panel with the specified events options', function () {
+		var map = new GMP({async: false, id: 'mymap', lat: 41.3833, lng: 2.1833});
+		var spy = sinon.spy();
+
+		var panel = map.addControl({
+			template: '<div><button id="clickMe">ctrl 1</button></div>',
+			events: {
+				'#clickMe click' : spy
+			}});
+
+		expect(spy).to.have.been.called;
+
+	});
+
+});
diff --git a/test/functional/map.Spec.js b/test/functional/map.Spec.js
index 290de72..f51f14b 100644
--- a/test/functional/map.Spec.js
+++ b/test/functional/map.Spec.js
@@ -11,7 +11,9 @@ describe('Given the Map Module', function () {
 
     it('should load a Google Maps Instance', function () {
       var result = map.load('mymap', {id: 'mymap'});
-      expect(result).to.eql({ type: 'text/javascript', src: '//maps.googleapis.com/maps/api/js?v=3.18&callback=GMP.maps.mymap.create' });
+
+      expect(result.type).to.equal('text/javascript');
+      expect(result.src).to.equal('//maps.googleapis.com/maps/api/js?v=3.18&callback=GMP.maps.mymap.create');
     });
 
   });
diff --git a/test/mocks/google-maps.js b/test/mocks/google-maps.js
index 25f71d1..8c1118a 100644
--- a/test/mocks/google-maps.js
+++ b/test/mocks/google-maps.js
@@ -10,6 +10,10 @@ module.exports = {
         cb()
       },
       addListener: function(instance, event, cb)
+      {
+        cb()
+      },
+			addDomListener: function(instance, event, cb)
       {
         cb()
       }
@@ -58,17 +62,34 @@ module.exports = {
       return {};
     },
     Map: function() {
-      return {gm_bindings_: {}, data: {
-        addGeoJson: function(json) {
-          return [{ag: {D: 53}, forEachProperty: function(){}, k: {}}, {ag: {D: 30}, forEachProperty: function(){}, k: {}}]
-        },
-        overrideStyle: function(feature, style) {
-          return feature.style = style;
-        }
-      },
-        setOptions: function(options) {
+      return {
+        gm_bindings_: {},
+        data: {
+					addGeoJson: function(json) {
+        		return [{ag: {D: 53}, forEachProperty: function(){}, k: {}}, {ag: {D: 30}, forEachProperty: function(){}, k: {}}]
+        	},
+        	overrideStyle: function(feature, style) {
+          	return feature.style = style;
+        	}
+      	},
+				setOptions: function(options) {
           this.options = options;
-        }
+        },
+				controls:{
+					'1': [], //TOP_LEFT
+					'2': [], //TOP_CENTER
+					'3': [], //TOP_RIGHT
+					'4': [],
+					'5': [],
+					'6': [],
+					'7': [],
+					'8': [],
+					'9': [],
+					'10': [],
+					'11': [],
+					'12': []
+		}
+
       };
     },
     Point: function() {
@@ -80,6 +101,25 @@ module.exports = {
     MapTypeId: {
       ROADMAP: 'ROADMAP',
       TERRAIN: 'TERRAIN'
+    },
+    ControlPosition: {
+	    BOTTOM: 11,
+	    BOTTOM_CENTER: 11,
+	    BOTTOM_LEFT: 10,
+	    BOTTOM_RIGHT: 12,
+	    CENTER: 13,
+	    LEFT: 5,
+	    LEFT_BOTTOM: 6,
+	    LEFT_CENTER: 4,
+	    LEFT_TOP: 5,
+	    RIGHT: 7,
+	    RIGHT_BOTTOM: 9,
+	    RIGHT_CENTER: 8,
+	    RIGHT_TOP: 7,
+	    TOP: 2,
+	    TOP_CENTER: 2,
+	    TOP_LEFT: 1,
+	    TOP_RIGHT: 3
     }
   }
 };
diff --git a/test/mocks/window.js b/test/mocks/window.js
index 17f5736..5fc8a26 100644
--- a/test/mocks/window.js
+++ b/test/mocks/window.js
@@ -2,7 +2,14 @@ module.exports = {
   document: {
     createElement: function()
     {
-      return {};
+      return {
+				style:{},
+				querySelectorAll: function(selector) {
+					return [{}];
+				},
+				querySelector: function(selector) {
+					return {};
+				}};
     },
 
     body: {