diff --git a/examples/partials/markers.html b/examples/partials/markers.html index 4c00e61..40a91b6 100644 --- a/examples/partials/markers.html +++ b/examples/partials/markers.html @@ -13,11 +13,11 @@

Map With Markers

height: 300px; } -/* +/* Fixes Bootstrap issues with Google Maps -see http://stackoverflow.com/a/9170756 +see http://stackoverflow.com/a/9170756 */ -.map img { +.map img { max-width: none; } @@ -43,7 +43,7 @@

Map With Markers

icon: 'https://maps.gstatic.com/mapfiles/ms2/micons/yellow-dot.png', } }; - + $scope.volcanoes = [ { id: 0, @@ -51,7 +51,7 @@

Map With Markers

img: 'http://www.thetrackerfoundation.org/Images/MountRainier_SM.jpg', elevationMeters: 4392, location: { - lat: 46.852947, + lat: 46.852947, lng: -121.760424 } }, @@ -61,7 +61,7 @@

Map With Markers

img: 'http://www.destination360.com/north-america/us/washington/images/s/washington-mt-baker-ski.jpg', elevationMeters: 3287, location: { - lat: 48.776797, + lat: 48.776797, lng: -121.814467 } }, @@ -71,7 +71,7 @@

Map With Markers

img: 'http://www.rhinoclimbs.com/Images/Glacier.9.jpg', elevationMeters: 3207, location: { - lat: 48.111844, + lat: 48.111844, lng: -121.11412 } } @@ -84,7 +84,7 @@

Map With Markers

$scope.options.notselected ); }; - + $scope.selectVolcano = function(volcano) { if ($scope.volcano) { $scope.volcano.selected = false; @@ -99,7 +99,7 @@

Map With Markers

+ diff --git a/src/controllers/angulargmMapController.js b/src/controllers/angulargmMapController.js index b411a60..88df52a 100644 --- a/src/controllers/angulargmMapController.js +++ b/src/controllers/angulargmMapController.js @@ -235,6 +235,22 @@ }; + /** + * Alias for google.maps.event.addDomListener(object, event, handler) + */ + this.addDomListener = function(object, event, handler) { + google.maps.event.addDomListener(object, event, handler); + }; + + + /** + * Alias for google.maps.event.addDomListenerOnce(object, event, handler) + */ + this.addDomListenerOnce = function(object, event, handler) { + google.maps.event.addDomListenerOnce(object, event, handler); + }; + + /** * Alias for google.maps.event.trigger(map, event) * @param {string} event an event defined on google.maps.Map @@ -251,9 +267,9 @@ google.maps.event.trigger(object, event); }; - this._newElement = function(type, opts) { + this._newElement = function(type, opts, CustomMarkerConstructor) { if (type === 'marker') { - return new angulargmDefaults.markerConstructor(opts); + return angular.isDefined(CustomMarkerConstructor) && angular.isFunction(CustomMarkerConstructor) ? new CustomMarkerConstructor(opts) : new angulargmDefaults.markerConstructor(opts); } else if (type === 'polyline') { if (!(opts.path instanceof Array)) { throw 'polylineOptions did not contain a path'; @@ -295,7 +311,7 @@ * @throw if any arguments are null/undefined or elementOptions does not * have all the required options (i.e. position) */ - this.addElement = function(type, scopeId, id, elementOptions, objectsName) { + this.addElement = function(type, scopeId, id, elementOptions, objectsName, customMarkerConstructor) { assertDefined(type, 'type'); assertDefined(scopeId, 'scopeId'); assertDefined(id, 'id'); @@ -319,7 +335,7 @@ // extend instead of copy to preserve value objects var opts = {}; angular.extend(opts, elementOptions); - var element = this._newElement(type, opts); + var element = this._newElement(type, opts, customMarkerConstructor); elements[scopeId][id] = element; element.setMap(this._map); diff --git a/src/directives/gmMarkers.js b/src/directives/gmMarkers.js index 5f4a990..f15ba5c 100644 --- a/src/directives/gmMarkers.js +++ b/src/directives/gmMarkers.js @@ -86,6 +86,16 @@ * multiple `gm-on-*event*` handlers, but only one for each type of event. * For events that have an underscore in their name, such as * 'position_changed', write it as 'gm-on-position-changed'. + * +* @param {expression} gm-marker-constructor an angular expression which evaluates + * to a custom marker constructor function (presumably based on + * [google.maps.OverlayView](https://developers.google.com/maps/documentation/javascript/reference#OverlayView). + * This constructor will be used for all markers created by this gm-markers + * directive. If not specified, the default from angulargmDefaults will be used. + * For example: + * ```html + * gm-marker-constructor="myCustomMarkerConstructor" + * ``` */ /** @@ -263,6 +273,7 @@ gmId: '&', gmPosition: '&', gmMarkerOptions: '&', + gmMarkerConstructor: '=', gmEvents: '&' }, require: '^gmMap', diff --git a/src/services/angulargmShape.js b/src/services/angulargmShape.js index a336b0e..7d8f3c6 100644 --- a/src/services/angulargmShape.js +++ b/src/services/angulargmShape.js @@ -51,7 +51,7 @@ * Create new shapes and add them to the map for objects which are not * currently on the map. */ - function _addNewElements(type, scope, controller, handlers, objectCache, optionsFn, objectsName) { + function _addNewElements(type, scope, controller, handlers, objectCache, optionsFn, objectsName, attrs) { var added = []; angular.forEach(objectCache, function(object, id) { var element = controller.getElement(type, scope.$id, id); @@ -64,7 +64,11 @@ if (element) { controller.updateElement(type, scope.$id, id, options); } else { - controller.addElement(type, scope.$id, id, options, objectsName); + if (type === 'marker' && attrs.gmMarkerConstructor) { + controller.addElement(type, scope.$id, id, options, objectsName, scope.gmMarkerConstructor); + } else { + controller.addElement(type, scope.$id, id, options, objectsName); + } element = controller.getElement(type, scope.$id, id); added.push({ id: id, @@ -73,7 +77,7 @@ // set up element event handlers angular.forEach(handlers, function(handler, event) { - controller.addListener(element, event, function() { + function listenerCb() { $timeout(function() { var context = {object: object}; context[type] = element; @@ -86,7 +90,19 @@ handler(scope.$parent.$parent.$parent , context); } }); - }); + } + if (type === 'marker' && attrs.gmMarkerConstructor) { + // nodeType check adapted from Lodash + if('getDomElement' in element && element.getDomElement() && element.getDomElement().nodeType === 1) { + controller.addDomListener(element.getDomElement(), event, listenerCb); + } else { + throw 'the constructor provided as gmMarkerConstructor must include a ' + + '"getDomElement" method that returns its DOM node, which must exist ' + + 'after the constructor is called'; + } + } else { + controller.addListener(element, event, listenerCb); + } }); } }); @@ -208,7 +224,7 @@ _addNewElements( type, scope, controller, handlers, - objectCache, elementOptions, objectsName + objectCache, elementOptions, objectsName, attrs ); _removeOrphanedElements(type, scope, controller, objectCache, objectsName); diff --git a/test/unit/directives/gmMarkersSpec.js b/test/unit/directives/gmMarkersSpec.js index edab3cb..a9379ca 100644 --- a/test/unit/directives/gmMarkersSpec.js +++ b/test/unit/directives/gmMarkersSpec.js @@ -213,6 +213,148 @@ describe('gmMarkers', function() { }); + describe('gmMarkerConstructor', function() { + var elm, scope, markersScopeId, mapCtrl, objectsName; + var latLngToObj; + var $timeout; + + var CustomMarker = function(options) { + if (options) { + if (options.position) { + this.latlng = new google.maps.LatLng(options.position.lat, options.position.lng); + } + this.div = document.createElement('div'); + if (options.map) { + this.setMap(options.map); + } + } + }; + CustomMarker.prototype = new google.maps.OverlayView(); + CustomMarker.prototype.setMap = function (map) { + this._map = map; + }; + CustomMarker.prototype.getPosition = function () { + return this.latlng; + }; + + beforeEach(inject(function($rootScope, $compile, _$timeout_, angulargmUtils) { + // set up scopes + scope = $rootScope.$new(); + scope.people2 = [ + {name: '0', id: 0, location: {lat: 1, lng: 2}}, + {name: '3', id: 3, location: {lat: 4, lng: 5}} + ]; + scope.getOpts2 = function(person) { + return { + key: 'value', + title: person.name + }; + }; + scope.mapId = 'test5'; + + $timeout = _$timeout_; + latLngToObj = angulargmUtils.latLngToObj; + + scope.myCustomMarker = CustomMarker; + + // compile angulargmMarkers directive + elm = angular.element('' + + '' + + '' + + ''); + + objectsName = 'people2'; + + $compile(elm)(scope); + + mapCtrl = elm.controller('gmMap'); + spyOn(scope.myCustomMarker.prototype, 'setMap').and.callThrough(); + spyOn(scope.myCustomMarker.prototype, 'getPosition').and.callThrough(); + spyOn(mapCtrl, 'addElement').and.callThrough(); + spyOn(mapCtrl, 'trigger').and.callThrough(); + spyOn(mapCtrl, 'addDomListener').and.callThrough(); + + markersScopeId = elm.find('gm-markers').isolateScope().$id; + + scope.$digest(); + $timeout.flush(); + })); + + it('is used when a custom marker constructor is provided', inject(function($compile) { + expect(mapCtrl.addElement.calls.count()).toEqual(scope.people2.length); + expect(mapCtrl.addElement).toHaveBeenCalledWith('marker', markersScopeId, + jasmine.any(String), {key: 'value', title: jasmine.any(String), position: jasmine.any(Object)}, objectsName, scope.myCustomMarker); + expect(scope.myCustomMarker.prototype.setMap.calls.count()).toEqual(scope.people2.length); + })); + + it('triggers events', function() { + var person = scope.people2[0]; + var position = person.location; + var id = person.name; + scope.markerEvents = [{ + event: 'click', + ids: [id], + }]; + + scope.$digest(); + $timeout.flush(); + var marker = mapCtrl.trigger.calls.mostRecent()['args'][0]; + var event = mapCtrl.trigger.calls.mostRecent()['args'][1]; + expect(latLngToObj(marker.getPosition())).toEqual(position); + expect(event).toEqual('click'); + }); + + + it('triggers events on multiple markers', function() { + + var position0 = scope.people2[0].location; + var position1 = scope.people2[1].location; + var id0 = scope.people2[0].name; + var id1 = scope.people2[1].name; + scope.markerEvents = [{ + event: 'click', + ids: [id0, id1] + }]; + scope.$digest(); + $timeout.flush(); + var marker0 = mapCtrl.trigger.calls.argsFor(0)[0]; + var marker1 = mapCtrl.trigger.calls.argsFor(1)[0]; + expect(latLngToObj(marker0.getPosition())).toEqual(position0); + expect(latLngToObj(marker1.getPosition())).toEqual(position1); + }); + + + it('triggers multiple events on markers', function() { + var position = scope.people2[0].location; + var id = scope.people2[0].name; + scope.markerEvents = [ + { + event: 'event0', + ids: [id] + }, + { + event: 'event1', + ids: [id] + } + ]; + scope.$digest(); + $timeout.flush(); + var event0 = mapCtrl.trigger.calls.argsFor(0)[1]; + var event1 = mapCtrl.trigger.calls.argsFor(1)[1]; + expect(event0).toEqual('event0'); + expect(event1).toEqual('event1'); + }); + + + }); + + it('triggers events', function() { var person = scope.people[0]; var position = person.location;