|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.io/license |
| 7 | + */ |
| 8 | + |
| 9 | +// Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 |
| 10 | +/// <reference types="google.maps" /> |
| 11 | + |
| 12 | +import { |
| 13 | + Input, |
| 14 | + OnDestroy, |
| 15 | + OnInit, |
| 16 | + Output, |
| 17 | + NgZone, |
| 18 | + Directive, |
| 19 | + OnChanges, |
| 20 | + SimpleChanges, |
| 21 | + inject, |
| 22 | + EventEmitter, |
| 23 | +} from '@angular/core'; |
| 24 | + |
| 25 | +import {GoogleMap} from '../google-map/google-map'; |
| 26 | +import {MapEventManager} from '../map-event-manager'; |
| 27 | +import {Observable} from 'rxjs'; |
| 28 | + |
| 29 | +/** |
| 30 | + * Default options for the Google Maps marker component. Displays a marker |
| 31 | + * at the Googleplex. |
| 32 | + */ |
| 33 | +export const DEFAULT_MARKER_OPTIONS = { |
| 34 | + position: {lat: 37.221995, lng: -122.184092}, |
| 35 | +}; |
| 36 | + |
| 37 | +/** |
| 38 | + * Angular component that renders a Google Maps marker via the Google Maps JavaScript API. |
| 39 | + * |
| 40 | + * See developers.google.com/maps/documentation/javascript/reference/marker |
| 41 | + */ |
| 42 | +@Directive({ |
| 43 | + selector: 'map-advanced-marker', |
| 44 | + exportAs: 'mapAdvancedMarker', |
| 45 | + standalone: true, |
| 46 | +}) |
| 47 | +export class MapAdvancedMarker implements OnInit, OnChanges, OnDestroy { |
| 48 | + private _eventManager = new MapEventManager(inject(NgZone)); |
| 49 | + |
| 50 | + /** |
| 51 | + * Rollover text. If provided, an accessibility text (e.g. for use with screen readers) will be added to the AdvancedMarkerElement with the provided value. |
| 52 | + * See: https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.title |
| 53 | + */ |
| 54 | + @Input() |
| 55 | + set title(title: string) { |
| 56 | + this._title = title; |
| 57 | + } |
| 58 | + private _title: string; |
| 59 | + |
| 60 | + /** |
| 61 | + * Sets the AdvancedMarkerElement's position. An AdvancedMarkerElement may be constructed without a position, but will not be displayed until its position is provided - for example, by a user's actions or choices. An AdvancedMarkerElement's position can be provided by setting AdvancedMarkerElement.position if not provided at the construction. |
| 62 | + * Note: AdvancedMarkerElement with altitude is only supported on vector maps. |
| 63 | + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.position |
| 64 | + */ |
| 65 | + @Input() |
| 66 | + set position( |
| 67 | + position: |
| 68 | + | google.maps.LatLngLiteral |
| 69 | + | google.maps.LatLng |
| 70 | + | google.maps.LatLngAltitude |
| 71 | + | google.maps.LatLngAltitudeLiteral, |
| 72 | + ) { |
| 73 | + this._position = position; |
| 74 | + } |
| 75 | + private _position: google.maps.LatLngLiteral | google.maps.LatLng; |
| 76 | + |
| 77 | + /** |
| 78 | + * The DOM Element backing the visual of an AdvancedMarkerElement. |
| 79 | + * Note: AdvancedMarkerElement does not clone the passed-in DOM element. Once the DOM element is passed to an AdvancedMarkerElement, passing the same DOM element to another AdvancedMarkerElement will move the DOM element and cause the previous AdvancedMarkerElement to look empty. |
| 80 | + * See: https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.content |
| 81 | + */ |
| 82 | + @Input() |
| 83 | + set content(content: Node | google.maps.marker.PinElement) { |
| 84 | + this._content = content; |
| 85 | + } |
| 86 | + private _content: Node; |
| 87 | + |
| 88 | + /** |
| 89 | + * If true, the AdvancedMarkerElement can be dragged. |
| 90 | + * Note: AdvancedMarkerElement with altitude is not draggable. |
| 91 | + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.gmpDraggable |
| 92 | + */ |
| 93 | + @Input() |
| 94 | + set gmpDraggable(draggable: boolean) { |
| 95 | + this._draggable = draggable; |
| 96 | + } |
| 97 | + private _draggable: boolean; |
| 98 | + |
| 99 | + /** |
| 100 | + * Options for constructing an AdvancedMarkerElement. |
| 101 | + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions |
| 102 | + */ |
| 103 | + @Input() |
| 104 | + set options(options: google.maps.marker.AdvancedMarkerElementOptions) { |
| 105 | + this._options = options; |
| 106 | + } |
| 107 | + private _options: google.maps.marker.AdvancedMarkerElementOptions; |
| 108 | + |
| 109 | + /** |
| 110 | + * AdvancedMarkerElements on the map are prioritized by zIndex, with higher values indicating higher display. |
| 111 | + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.zIndex |
| 112 | + */ |
| 113 | + @Input() |
| 114 | + set zIndex(zIndex: number) { |
| 115 | + this._zIndex = zIndex; |
| 116 | + } |
| 117 | + private _zIndex: number; |
| 118 | + |
| 119 | + /** |
| 120 | + * This event is fired when the AdvancedMarkerElement element is clicked. |
| 121 | + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.click |
| 122 | + */ |
| 123 | + @Output() readonly mapClick: Observable<google.maps.MapMouseEvent> = |
| 124 | + this._eventManager.getLazyEmitter<google.maps.MapMouseEvent>('click'); |
| 125 | + |
| 126 | + /** |
| 127 | + * This event is repeatedly fired while the user drags the AdvancedMarkerElement. |
| 128 | + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.drag |
| 129 | + */ |
| 130 | + @Output() readonly mapDrag: Observable<google.maps.MapMouseEvent> = |
| 131 | + this._eventManager.getLazyEmitter<google.maps.MapMouseEvent>('drag'); |
| 132 | + |
| 133 | + /** |
| 134 | + * This event is fired when the user stops dragging the AdvancedMarkerElement. |
| 135 | + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.dragend |
| 136 | + */ |
| 137 | + @Output() readonly mapDragend: Observable<google.maps.MapMouseEvent> = |
| 138 | + this._eventManager.getLazyEmitter<google.maps.MapMouseEvent>('dragend'); |
| 139 | + |
| 140 | + /** |
| 141 | + * This event is fired when the user starts dragging the AdvancedMarkerElement. |
| 142 | + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.dragstart |
| 143 | + */ |
| 144 | + @Output() readonly mapDragstart: Observable<google.maps.MapMouseEvent> = |
| 145 | + this._eventManager.getLazyEmitter<google.maps.MapMouseEvent>('dragstart'); |
| 146 | + |
| 147 | + /** Event emitted when the marker is initialized. */ |
| 148 | + @Output() readonly markerInitialized: EventEmitter<google.maps.marker.AdvancedMarkerElement> = |
| 149 | + new EventEmitter<google.maps.marker.AdvancedMarkerElement>(); |
| 150 | + |
| 151 | + /** |
| 152 | + * The underlying google.maps.marker.AdvancedMarkerElement object. |
| 153 | + * |
| 154 | + * See developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement |
| 155 | + */ |
| 156 | + advancedMarker: google.maps.marker.AdvancedMarkerElement; |
| 157 | + |
| 158 | + constructor( |
| 159 | + private readonly _googleMap: GoogleMap, |
| 160 | + private _ngZone: NgZone, |
| 161 | + ) {} |
| 162 | + |
| 163 | + ngOnInit() { |
| 164 | + if (!this._googleMap._isBrowser) { |
| 165 | + return; |
| 166 | + } |
| 167 | + if (google.maps.marker?.AdvancedMarkerElement && this._googleMap.googleMap) { |
| 168 | + this._initialize(this._googleMap.googleMap, google.maps.marker.AdvancedMarkerElement); |
| 169 | + } else { |
| 170 | + this._ngZone.runOutsideAngular(() => { |
| 171 | + Promise.all([this._googleMap._resolveMap(), google.maps.importLibrary('marker')]).then( |
| 172 | + ([map, lib]) => { |
| 173 | + this._initialize(map, (lib as google.maps.MarkerLibrary).AdvancedMarkerElement); |
| 174 | + }, |
| 175 | + ); |
| 176 | + }); |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + private _initialize( |
| 181 | + map: google.maps.Map, |
| 182 | + advancedMarkerConstructor: typeof google.maps.marker.AdvancedMarkerElement, |
| 183 | + ) { |
| 184 | + // Create the object outside the zone so its events don't trigger change detection. |
| 185 | + // We'll bring it back in inside the `MapEventManager` only for the events that the |
| 186 | + // user has subscribed to. |
| 187 | + this._ngZone.runOutsideAngular(() => { |
| 188 | + this.advancedMarker = new advancedMarkerConstructor(this._combineOptions()); |
| 189 | + this._assertInitialized(); |
| 190 | + this.advancedMarker.map = map; |
| 191 | + this._eventManager.setTarget(this.advancedMarker); |
| 192 | + this.markerInitialized.next(this.advancedMarker); |
| 193 | + }); |
| 194 | + } |
| 195 | + |
| 196 | + ngOnChanges(changes: SimpleChanges) { |
| 197 | + const {advancedMarker, _content, _position, _title, _draggable, _zIndex} = this; |
| 198 | + if (advancedMarker) { |
| 199 | + if (changes['title']) { |
| 200 | + advancedMarker.title = _title; |
| 201 | + } |
| 202 | + |
| 203 | + if (changes['content']) { |
| 204 | + advancedMarker.content = _content; |
| 205 | + } |
| 206 | + |
| 207 | + if (changes['gmpDraggable']) { |
| 208 | + advancedMarker.gmpDraggable = _draggable; |
| 209 | + } |
| 210 | + |
| 211 | + if (changes['content']) { |
| 212 | + advancedMarker.content = _content; |
| 213 | + } |
| 214 | + |
| 215 | + if (changes['position']) { |
| 216 | + advancedMarker.position = _position; |
| 217 | + } |
| 218 | + |
| 219 | + if (changes['zIndex']) { |
| 220 | + advancedMarker.zIndex = _zIndex; |
| 221 | + } |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + ngOnDestroy() { |
| 226 | + this.markerInitialized.complete(); |
| 227 | + this._eventManager.destroy(); |
| 228 | + } |
| 229 | + |
| 230 | + /** Creates a combined options object using the passed-in options and the individual inputs. */ |
| 231 | + private _combineOptions(): google.maps.marker.AdvancedMarkerElementOptions { |
| 232 | + const options = this._options || DEFAULT_MARKER_OPTIONS; |
| 233 | + return { |
| 234 | + ...options, |
| 235 | + title: this._title || options.title, |
| 236 | + position: this._position || options.position, |
| 237 | + content: this._content || options.content, |
| 238 | + zIndex: this._zIndex ?? options.zIndex, |
| 239 | + gmpDraggable: this._draggable ?? options.gmpDraggable, |
| 240 | + map: this._googleMap.googleMap, |
| 241 | + }; |
| 242 | + } |
| 243 | + |
| 244 | + /** Asserts that the map has been initialized. */ |
| 245 | + private _assertInitialized(): asserts this is {marker: google.maps.marker.AdvancedMarkerElement} { |
| 246 | + if (typeof ngDevMode === 'undefined' || ngDevMode) { |
| 247 | + if (!this.advancedMarker) { |
| 248 | + throw Error( |
| 249 | + 'Cannot interact with a Google Map Marker before it has been ' + |
| 250 | + 'initialized. Please wait for the Marker to load before trying to interact with it.', |
| 251 | + ); |
| 252 | + } |
| 253 | + } |
| 254 | + } |
| 255 | +} |
0 commit comments