diff --git a/src/directives-const.ts b/src/directives-const.ts index df1220acb..6d7543a36 100644 --- a/src/directives-const.ts +++ b/src/directives-const.ts @@ -1,4 +1,6 @@ import {SebmGoogleMap} from './directives/google-map'; import {SebmGoogleMapMarker} from './directives/google-map-marker'; +import {SebmGoogleMapInfoWindow} from './directives/google-map-info-window'; -export const ANGULAR2_GOOGLE_MAPS_DIRECTIVES: any[] = [SebmGoogleMap, SebmGoogleMapMarker]; +export const ANGULAR2_GOOGLE_MAPS_DIRECTIVES: any[] = + [SebmGoogleMap, SebmGoogleMapMarker, SebmGoogleMapInfoWindow]; diff --git a/src/directives.ts b/src/directives.ts index ce0a8e158..0cd668fde 100644 --- a/src/directives.ts +++ b/src/directives.ts @@ -1,3 +1,4 @@ export {SebmGoogleMap} from './directives/google-map'; export {SebmGoogleMapMarker} from './directives/google-map-marker'; +export {SebmGoogleMapInfoWindow} from './directives/google-map-info-window'; export {ANGULAR2_GOOGLE_MAPS_DIRECTIVES} from './directives-const'; diff --git a/src/directives/google-map-info-window.ts b/src/directives/google-map-info-window.ts new file mode 100644 index 000000000..2e91abfbf --- /dev/null +++ b/src/directives/google-map-info-window.ts @@ -0,0 +1,147 @@ +import {Component, SimpleChange, OnDestroy, OnChanges, ElementRef} from 'angular2/core'; +import {InfoWindowManager} from '../services/info-window-manager'; +import {SebmGoogleMapMarker} from './google-map-marker'; + +let infoWindowId = 0; + +/** + * SebmGoogleMapInfoWindow renders a info window inside a {@link SebmGoogleMapMarker} or standalone. + * + * ### Example + * ```typescript + * import {Component} from 'angular2/core'; + * import {SebmGoogleMap, SebmGoogleMapMarker, SebmGoogleMapInfoWindow} from + * 'angular2-google-maps/core'; + * + * @Component({ + * selector: 'my-map-cmp', + * directives: [SebmGoogleMap, SebmGoogleMapMarker, SebmGoogleMapInfoWindow], + * styles: [` + * .sebm-google-map-container { + * height: 300px; + * } + * `], + * template: ` + * + * + * + * Hi, this is the content of the info window + * + * + * + * ` + * }) + * ``` + */ +@Component({ + selector: 'sebm-google-map-info-window', + inputs: ['latitude', 'longitude', 'disableAutoPan'], + template: ` +
+ +
+ ` +}) +export class SebmGoogleMapInfoWindow implements OnDestroy, + OnChanges { + /** + * The latitude position of the info window (only usefull if you use it ouside of a {@link + * SebmGoogleMapMarker}). + */ + latitude: number; + + /** + * The longitude position of the info window (only usefull if you use it ouside of a {@link + * SebmGoogleMapMarker}). + */ + longitude: number; + + /** + * Disable auto-pan on open. By default, the info window will pan the map so that it is fully + * visible when it opens. + */ + disableAutoPan: boolean; + + /** + * All InfoWindows are displayed on the map in order of their zIndex, with higher values + * displaying in front of InfoWindows with lower values. By default, InfoWindows are displayed + * according to their latitude, with InfoWindows of lower latitudes appearing in front of + * InfoWindows at higher latitudes. InfoWindows are always displayed in front of markers. + */ + zIndex: number; + + /** + * Maximum width of the infowindow, regardless of content's width. This value is only considered + * if it is set before a call to open. To change the maximum width when changing content, call + * close, update maxWidth, and then open. + */ + maxWidth: number; + + /** + * Holds the marker that is the host of the info window (if available) + */ + hostMarker: SebmGoogleMapMarker; + + /** + * Holds the native element that is used for the info window content. + */ + content: Node; + + private static _infoWindowOptionsInputs: string[] = ['disableAutoPan', 'maxWidth']; + private _infoWindowAddedToManager: boolean = false; + private _id: string = (infoWindowId++).toString(); + + constructor(private _infoWindowManager: InfoWindowManager, private _el: ElementRef) {} + + ngOnInit() { + this.content = this._el.nativeElement.querySelector('.sebm-google-map-info-window-content'); + this._addToManager(); + } + + /** @internal */ + ngOnChanges(changes: {[key: string]: SimpleChange}) { + this._addToManager(); + if ((changes['latitude'] || changes['longitude']) && typeof this.latitude === 'number' && + typeof this.longitude === 'number') { + this._infoWindowManager.setPosition(this); + } + if (changes['zIndex']) { + this._infoWindowManager.setZIndex(this); + } + this._setInfoWindowOptions(changes); + } + + private _setInfoWindowOptions(changes: {[key: string]: SimpleChange}) { + let options: {[propName: string]: any} = {}; + let optionKeys = Object.keys(changes).filter( + k => SebmGoogleMapInfoWindow._infoWindowOptionsInputs.indexOf(k) !== -1); + optionKeys.forEach((k) => { options[k] = changes[k].currentValue; }); + this._infoWindowManager.setOptions(this, options); + } + + private _addToManager() { + if (!this._infoWindowAddedToManager) { + this._infoWindowAddedToManager = true; + this._infoWindowManager.addInfoWindow(this); + } + } + + /** + * Opens the info window. + */ + open(): Promise { return this._infoWindowManager.open(this); } + + /** + * Closes the info window. + */ + close(): Promise { return this._infoWindowManager.close(this); } + + /** @internal */ + id(): string { return this._id; } + + /** @internal */ + toString(): string { return 'SebmGoogleMapInfoWindow-' + this._id.toString(); } + + /** @internal */ + ngOnDestroy() { this._infoWindowManager.deleteInfoWindow(this); } +} diff --git a/src/directives/google-map-marker.ts b/src/directives/google-map-marker.ts index d67eb4f74..2214ed72b 100644 --- a/src/directives/google-map-marker.ts +++ b/src/directives/google-map-marker.ts @@ -1,5 +1,14 @@ -import {Directive, SimpleChange, OnDestroy, OnChanges, EventEmitter} from 'angular2/core'; +import { + Directive, + SimpleChange, + OnDestroy, + OnChanges, + EventEmitter, + ContentChild, + AfterContentInit +} from 'angular2/core'; import {MarkerManager} from '../services/marker-manager'; +import {SebmGoogleMapInfoWindow} from './google-map-info-window'; import {MouseEvent} from '../events'; import * as mapTypes from '../services/google-maps-types'; @@ -36,7 +45,7 @@ let markerId = 0; outputs: ['markerClick', 'dragEnd'] }) export class SebmGoogleMapMarker implements OnDestroy, - OnChanges { + OnChanges, AfterContentInit { /** * The latitude position of the marker. */ @@ -77,11 +86,20 @@ export class SebmGoogleMapMarker implements OnDestroy, */ dragEnd: EventEmitter = new EventEmitter(); + @ContentChild(SebmGoogleMapInfoWindow) private _infoWindow: SebmGoogleMapInfoWindow; + private _markerAddedToManger: boolean = false; private _id: string; constructor(private _markerManager: MarkerManager) { this._id = (markerId++).toString(); } + /* @internal */ + ngAfterContentInit() { + if (this._infoWindow != null) { + this._infoWindow.hostMarker = this; + } + } + /** @internal */ ngOnChanges(changes: {[key: string]: SimpleChange}) { if (typeof this.latitude !== 'number' || typeof this.longitude !== 'number') { @@ -112,6 +130,9 @@ export class SebmGoogleMapMarker implements OnDestroy, private _addEventListeners() { this._markerManager.createEventObservable('click', this).subscribe(() => { + if (this._infoWindow != null) { + this._infoWindow.open(); + } this.markerClick.next(null); }); this._markerManager.createEventObservable('dragend', this) diff --git a/src/directives/google-map.ts b/src/directives/google-map.ts index 7bd489315..10309cfd8 100644 --- a/src/directives/google-map.ts +++ b/src/directives/google-map.ts @@ -1,6 +1,7 @@ import {Component, ElementRef, EventEmitter, OnChanges, OnInit, SimpleChange} from 'angular2/core'; import {GoogleMapsAPIWrapper} from '../services/google-maps-api-wrapper'; import {MarkerManager} from '../services/marker-manager'; +import {InfoWindowManager} from '../services/info-window-manager'; import {LatLng, LatLngLiteral} from '../services/google-maps-types'; import {MouseEvent} from '../events'; @@ -31,7 +32,7 @@ import {MouseEvent} from '../events'; */ @Component({ selector: 'sebm-google-map', - providers: [GoogleMapsAPIWrapper, MarkerManager], + providers: [GoogleMapsAPIWrapper, MarkerManager, InfoWindowManager], inputs: [ 'longitude', 'latitude', 'zoom', 'disableDoubleClickZoom', 'disableDefaultUI', 'scrollwheel', 'backgroundColor', 'draggableCursor', 'draggingCursor', 'keyboardShortcuts', 'zoomControl' @@ -43,10 +44,15 @@ import {MouseEvent} from '../events'; width: inherit; height: inherit; } + .sebm-google-map-content { + display:none; + } `], template: `
- +
+ +
` }) export class SebmGoogleMap implements OnChanges, diff --git a/src/services/google-maps-api-wrapper.ts b/src/services/google-maps-api-wrapper.ts index cdaee1d5a..6e54b4d6d 100644 --- a/src/services/google-maps-api-wrapper.ts +++ b/src/services/google-maps-api-wrapper.ts @@ -45,6 +45,10 @@ export class GoogleMapsAPIWrapper { }); } + createInfoWindow(options?: mapTypes.InfoWindowOptions): Promise { + return this._map.then(() => { return new google.maps.InfoWindow(options); }); + } + subscribeToMapEvent(eventName: string): Observable { return Observable.create((observer: Observer) => { this._map.then((m: mapTypes.GoogleMap) => { diff --git a/src/services/google-maps-types.ts b/src/services/google-maps-types.ts index cdaba284a..d7bcc4574 100644 --- a/src/services/google-maps-types.ts +++ b/src/services/google-maps-types.ts @@ -64,3 +64,35 @@ export interface MapOptions { keyboardShortcuts?: boolean; zoomControl?: boolean; } + +export interface InfoWindow { + constructor(opts?: InfoWindowOptions): void; + close(): void; + getContent(): string | Node; + getPosition(): LatLng; + getZIndex(): number; + open(map?: GoogleMap, anchor?: MVCObject): void; + setContent(content: string | Node): void; + setOptions(options: InfoWindowOptions): void; + setPosition(position: LatLng | LatLngLiteral): void; + setZIndex(zIndex: number): void; +} + +export interface MVCObject { constructor(): void; } + +export interface Size { + height: number; + width: number; + constructor(width: number, height: number, widthUnit?: string, heightUnit?: string): void; + equals(other: Size): boolean; + toString(): string; +} + +export interface InfoWindowOptions { + content?: string | Node; + disableAutoPan?: boolean; + maxWidth?: number; + pixelOffset?: Size; + position?: LatLng | LatLngLiteral; + zIndex?: number; +} diff --git a/src/services/info-window-manager.ts b/src/services/info-window-manager.ts new file mode 100644 index 000000000..3f6438ae8 --- /dev/null +++ b/src/services/info-window-manager.ts @@ -0,0 +1,71 @@ +import {Injectable, NgZone} from 'angular2/core'; +import {SebmGoogleMapInfoWindow} from '../directives/google-map-info-window'; +import {GoogleMapsAPIWrapper} from './google-maps-api-wrapper'; +import {MarkerManager} from './marker-manager'; +import {InfoWindow, InfoWindowOptions} from './google-maps-types'; + +@Injectable() +export class InfoWindowManager { + private _infoWindows: Map> = + new Map>(); + + constructor( + private _mapsWrapper: GoogleMapsAPIWrapper, private _zone: NgZone, + private _markerManager: MarkerManager) {} + + deleteInfoWindow(infoWindow: SebmGoogleMapInfoWindow): Promise { + const iWindow = this._infoWindows.get(infoWindow); + if (iWindow == null) { + // info window already deleted + return Promise.resolve(); + } + return iWindow.then((i: InfoWindow) => { + return this._zone.run(() => { + i.close(); + this._infoWindows.delete(infoWindow); + }); + }); + } + + setPosition(infoWindow: SebmGoogleMapInfoWindow): Promise { + return this._infoWindows.get(infoWindow).then((i: InfoWindow) => i.setPosition({ + lat: infoWindow.latitude, + lng: infoWindow.longitude + })); + } + + setZIndex(infoWindow: SebmGoogleMapInfoWindow): Promise { + return this._infoWindows.get(infoWindow) + .then((i: InfoWindow) => i.setZIndex(infoWindow.zIndex)); + } + + open(infoWindow: SebmGoogleMapInfoWindow): Promise { + return this._infoWindows.get(infoWindow).then((w) => { + if (infoWindow.hostMarker != null) { + return this._markerManager.getNativeMarker(infoWindow.hostMarker).then((marker) => { + return this._mapsWrapper.getMap().then((map) => w.open(map, marker)); + }); + } + return this._mapsWrapper.getMap().then((map) => w.open(map)); + }); + } + + close(infoWindow: SebmGoogleMapInfoWindow): Promise { + return this._infoWindows.get(infoWindow).then((w) => w.close()); + } + + setOptions(infoWindow: SebmGoogleMapInfoWindow, options: InfoWindowOptions) { + return this._infoWindows.get(infoWindow).then((i: InfoWindow) => i.setOptions(options)); + } + + addInfoWindow(infoWindow: SebmGoogleMapInfoWindow) { + const options: InfoWindowOptions = { + content: infoWindow.content, + }; + if (typeof infoWindow.latitude === 'number' && typeof infoWindow.longitude === 'number') { + options.position = {lat: infoWindow.latitude, lng: infoWindow.longitude}; + } + const infoWindowPromise = this._mapsWrapper.createInfoWindow(options); + this._infoWindows.set(infoWindow, infoWindowPromise); + } +} diff --git a/src/services/marker-manager.ts b/src/services/marker-manager.ts index 5e46ebec5..6a087e3c0 100644 --- a/src/services/marker-manager.ts +++ b/src/services/marker-manager.ts @@ -57,6 +57,10 @@ export class MarkerManager { this._markers.set(marker, markerPromise); } + getNativeMarker(marker: SebmGoogleMapMarker): Promise { + return this._markers.get(marker); + } + createEventObservable(eventName: string, marker: SebmGoogleMapMarker): Observable { return Observable.create((observer: Observer) => { this._markers.get(marker).then((m: Marker) => {