PageSourceSearch

https://www.notintheguidebooks.com/wp-content/plugins/acf-google-m…/assets/js/input.js?ver=1.0.5

js notintheguidebooks.com collected 2026-10-06 13:45:41 UTC 22,567 bytes, 875 lines download raw bytes

1(function($){
2
3	"use strict";
4	
5	/**
6	*  initialize_field
7	*
8	*  This function will initialize the $field.
9	*
10	*  @date	30/11/17
11	*  @since	5.6.5
12	*
13	*  @param	n/a
14	*  @return	n/a
15	*/
16
17	function initialize_field( $field ) {
18		// Nothing here yet.
19	}
20	
21	
22	if( typeof acf.add_action !== 'undefined' ) {
23	
24		/*
25		*  ready & append (ACF5)
26		*
27		*  These two events are called when a field element is ready for initizliation.
28		*  - ready: on page load similar to $(document).ready()
29		*  - append: on new DOM elements appended via repeater field or other AJAX calls
30		*
31		*  @param	n/a
32		*  @return	n/a
33		*/
34		
35		acf.add_action('ready_field/type=google_map_multi', initialize_field);
36		acf.add_action('append_field/type=google_map_multi', initialize_field);
37		
38		
39	} else {
40		
41		/*
42		*  acf/setup_fields (ACF4)
43		*
44		*  These single event is called when a field element is ready for initizliation.
45		*
46		*  @param	event		an event object. This can be ignored
47		*  @param	element		An element which contains the new HTML
48		*  @return	n/a
49		*/
50		
51		$(document).on('acf/setup_fields', function(e, postbox){
52			
53			// find all relevant fields
54			$(postbox).find('.field[data-field_type="google_map_multi"]').each(function(){
55				
56				// initialize
57				initialize_field( $(this) );
58				
59			});
60		
61		});
62	
63	}
64
65	var Field = acf.Field.extend({
66
67		type: 'google_map_multi',
68
69		map: false,
70
71		wait: 'load',
72
73		events: {
74			'click a[data-name="clear"]': 		'onClickClear',
75			'click a[data-name="locate"]': 		'onClickLocate',
76			'click a[data-name="search"]': 		'onClickSearch',
77			'keydown .search': 					'onKeydownSearch',
78			'keyup .search': 					'onKeyupSearch',
79			'focus .search': 					'onFocusSearch',
80			'blur .search': 					'onBlurSearch',
81			'showField':						'onShow',
82		},
83
84		// List of values in parsed JSON.
85		// Used only for the hidden ACF field that holds the field's values.
86		vals: [],
87
88		markers: [],
89
90		draggedMarkerIndex: '',
91
92		$control: function(){
93			return this.$('.acf-google-map-multi');
94		},
95
96		$search: function(){
97			return this.$('.search');
98		},
99
100		$canvas: function(){
101			return this.$('.canvas');
102		},
103
104		$markerList: function(){
105			return this.$('.gmm-markers-list');
106		},
107
108		setState: function( state ){
109
110			// Remove previous state classes.
111			this.$control().removeClass( '-value -loading -searching' );
112
113			// Determine auto state based of current value.
114			if( state === 'default' ) {
115				state = this.val() ? 'value' : '';
116			}
117
118			// Update state class.
119			if( state ) {
120				this.$control().addClass( '-' + state );
121			}
122		},
123
124		getValue: function(){
125			var val = this.$input().val();
126			if( val ) {
127				return JSON.parse( val )
128			} else {
129				return false;
130			}
131		},
132
133		setValue: function( val, silent ){
134
135			// Convert input value.
136			var valAttr = '';
137			if( val ) {
138				valAttr = JSON.stringify( val );
139			}
140
141			// Update input (with change).
142			acf.val( this.$input(), valAttr );
143
144			// Bail early if silent update.
145			if( silent ) {
146				return;
147			}
148
149			// Render.
150			this.renderVal( val );
151
152			/**
153			 * Fires immediately after the value has changed.
154			 *
155			 * @date	12/02/2014
156			 * @since	5.0.0
157			 *
158			 * @param	object|string val The new value.
159			 * @param	object map The Google Map isntance.
160			 * @param	object field The field instance.
161			 */
162			acf.doAction('google_map_multi_change', val, this.map, this);
163		},
164
165		renderVal: function( val ){
166
167			// Value.
168			if( val ) {
169				this.setState( 'value' );
170				this.$search().val( val.address );
171				// this.setPosition( val.lat, val.lng );
172
173				// No value.
174			} else {
175				this.setState( '' );
176				this.$search().val( '' );
177				this.map.marker.setVisible( false );
178			}
179		},
180
181		newLatLng: function( lat, lng ){
182			return new google.maps.LatLng( parseFloat(lat), parseFloat(lng) );
183		},
184
185		initialize: function(){
186			// Ensure Google API is loaded and then initialize map.
187			gmmWithAPI( this.initializeMap.bind(this) );
188		},
189
190		initializeMap: function(){
191			if (!gmmGeocoder) {
192				gmmGeocoder = new google.maps.Geocoder();
193			}
194
195			// Get value ignoring conditional logic status.
196			var val = this.getValue();
197
198			// Construct default args.
199			var args = acf.parseArgs(val, {
200				zoom: this.get('zoom'),
201				lat: this.get('lat'),
202				lng: this.get('lng')
203			});
204
205			// Create Map.
206			var mapArgs = {
207				scrollwheel:	false,
208				mapTypeId:		google.maps.MapTypeId.ROADMAP,
209				marker:			{
210					draggable: 		true,
211					raiseOnDrag: 	true
212				},
213				autocomplete: {}
214			};
215			mapArgs = acf.applyFilters( 'google_map_multi_args', mapArgs, this );
216			var map = new google.maps.Map( this.$canvas()[0], mapArgs );
217
218			// Maybe Create Autocomplete.
219			var autocomplete = false;
220			if( acf.isset(google, 'maps', 'places', 'Autocomplete') ) {
221				var autocompleteArgs = mapArgs.autocomplete || {};
222				autocompleteArgs = acf.applyFilters('google_map_autocomplete_args', autocompleteArgs, this);
223				autocomplete = new google.maps.places.Autocomplete( this.$search()[0], autocompleteArgs );
224				autocomplete.bindTo('bounds', map);
225			}
226
227			// Add map events.
228			this.addMapEvents( this, map, autocomplete );
229
230			// Remove any residual data from our marker list. At this point, we shouldn't have anything in the list.
231			this.markers = [];
232
233			// Sets up all the markers if this field has values.
234			if( val ) {
235				// Create bounds.
236				var bounds = new google.maps.LatLngBounds();
237
238				this.vals = val;
239				for (let i = 0; i < this.vals.length; i++) {
240					var latLng = { lat: this.vals[i].lat, lng: this.vals[i].lng };
241
242					this.markers.push({
243						values: this.vals[i],
244						marker: {},
245					});
246
247					this.addMarker( latLng, map );
248					bounds.extend( latLng );
249				}
250
251				if (this.vals.length === 1) {
252					// If we only have 1 marker, set our map's zoom and center to that marker's values.
253					map.setZoom(this.vals[0].zoom);
254					map.setCenter( {lat: this.vals[0].lat, lng: this.vals[0].lng} );
255				}
256				else {
257					// Tell our map to fit the bounds defined by our markers.
258					map.fitBounds( bounds );
259				}
260
261			}
262			else {
263				// We set the zoom and center to default values if there are no markers.
264				map.setZoom(8);
265				map.setCenter( {lat: 51.4934, lng: 0.0098} );
266
267				// Reset our vals storage for the new field with no data yet.
268				this.vals = [];
269			}
270
271			// Append references.
272			map.acf = this;
273			map.autocomplete = autocomplete;
274
275			// Render the initial markers list under the map.
276			this.renderMarkerList();
277
278			// Set this object's map property to the map we've just created.
279			// Don't change the map in this function after this line.
280			this.map = map;
281
282			/**
283			 * Fires immediately after the Google Map has been initialized.
284			 *
285			 * @date	12/02/2014
286			 * @since	5.0.0
287			 *
288			 * @param	object map The Google Map instance.
289			 * @param	object marker The Google Map marker instance.
290			 * @param	object field The field instance.
291			 */
292			acf.doAction('google_map_multi_init', map, this);
293
294			// Remove the loading icon.
295			$('.acf-field-google-map-multi .acf-loading').remove();
296		},
297
298		addMapEvents: function( field, map, autocomplete ){
299
300			// Click map.
301			google.maps.event.addListener( map, 'click', function( e ) {
302				if (field.validateMaxPins()) {
303					var lat = e.latLng.lat();
304					var lng = e.latLng.lng();
305
306					field.searchPosition( lat, lng );
307					field.addMarker( e.latLng );
308				}
309			});
310
311			// Autocomplete search.
312			if( autocomplete ) {
313				google.maps.event.addListener(autocomplete, 'place_changed', function() {
314					if (field.validateMaxPins()) {
315						var place = this.getPlace();
316						field.searchPlace(place);
317
318						// If places.geometry doesn't exist, it means we've had to geocode an address. Adding the marker and
319						// the rest would be done in that geocode callback.
320						if (place.geometry !== undefined) {
321							var latLng = {lat: place.geometry.location.lat(), lng: place.geometry.location.lng()};
322							field.addMarker(latLng);
323
324							// Add the new place just searched to the bounds and fit the map to these new bounds so the map
325							// expands to show the new marker that's just been added.
326							map.fitBounds(field.createMapBounds(field));
327
328							// Re-render the list of markers under the map.
329							field.renderMarkerList();
330						}
331					}
332				});
333			}
334
335			// Detect zoom change.
336			google.maps.event.addListener( map, 'zoom_changed', function(){
337				var val = field.val();
338				if( val ) {
339					val.zoom = map.getZoom();
340					field.setValue( val, true );
341				}
342			});
343		},
344
345		/**
346		 *
347		 * @param latLng    latLng object.
348		 * @param map       The Google Map object. If not given, it will try to use this field object's map property.
349		 * @param field
350		 * @returns {*}
351		 */
352		addMarker: function( latLng, map = this.map, field = this ) {
353			let markerArgs = {
354				draggable: true,
355				raiseOnDrag: true,
356				position: latLng,
357				map: map
358			};
359			markerArgs = acf.applyFilters('google_map_multi_marker_args', markerArgs, field);
360			let marker = new google.maps.Marker( markerArgs );
361
362			// Add the new marker to our list of markers.
363			field.markers[field.markers.length - 1].marker = marker;
364
365			// Add a right-click event to allow users to remove this marker.
366			marker.addListener('rightclick', function() {
367				field.removeMarker( marker, field );
368			});
369
370			// Remove the marker from the data at the start of dragging a marker.
371			marker.addListener('dragstart', function () {
372				let markerIndex = field.markers.findIndex(element => marker === element.marker );
373				field.removeMarkerDataFromFieldData( marker, field, markerIndex );
374				field.markers.splice( markerIndex, 1 );
375			});
376
377			// Add a drag-end event to update the marker's data when it's dragged on the map.
378			marker.addListener('dragend', function () {
379				// TODO Try to update the existing marker that's being dragged instead of creating a new item in the list.
380				field.searchPosition( marker.position.lat(), marker.position.lng() );
381				field.markers[field.markers.length - 1].marker = marker;
382			});
383
384			return marker;
385		},
386
387		removeMarker: function( marker, field = this ) {
388			// Remove the marker from this object's markersArray property.
389			var markerIndex = field.markers.findIndex(element => marker === element.marker );
390
391			// Check if an index was found before we remove anything.
392			if (markerIndex >= 0) {
393				// Remove the marker's data from the hidden field of JSON.
394				field.removeMarkerDataFromFieldData(marker, field, markerIndex);
395				field.markers.splice(markerIndex, 1);
396
397				// Re-render the list of markers under the map after removing it from our values.
398				this.renderMarkerList();
399			}
400
401			// Remove the marker from the map.
402			marker.setMap(null);
403		},
404
405		removeMarkerDataFromFieldData: function( marker, field = this, markerIndex = false ) {
406			if (!markerIndex) {
407				// If the markerIndex hasn't been provided, we need to find it.
408				markerIndex = field.markers.findIndex(element => marker === element.marker );
409			}
410			field.vals.splice(markerIndex, 1);
411			field.val( this.vals );
412		},
413
414		/**
415		 * Checks if the maximum number of pins has been added to the current map.
416		 *
417		 * @return boolean
418		 */
419		validateMaxPins: function() {
420			// Check for no limit.
421			if (this.data.maxPins <= 0) {
422				return true;
423			}
424
425			// Check the limit against the current amount of markers.
426			if (this.markers.length < this.data.maxPins) {
427				return true;
428			}
429			else {
430				this.showNotice({
431					text: acf.__('You can\'t add anymore pins. The maximum number of pins is ' + this.data.maxPins),
432					type: 'warning'
433				});
434
435				return false;
436			}
437		},
438
439		/**
440		 * Creates Google Map LatLngBounds for all the markers in this object's markersArray property.
441		 *
442		 * @param field
443		 */
444		createMapBounds: function( field = this ) {
445			var newBounds = new google.maps.LatLngBounds();
446
447			for ( let i = 0; i < field.markers.length; i++ ) {
448				newBounds.extend( {lat: field.markers[i].marker.position.lat(), lng: field.markers[i].marker.position.lng()} );
449			}
450
451			return newBounds;
452		},
453
454		renderMarkerList: function() {
455			this.$markerList().empty();
456			for (let i = 0; i < this.markers.length; i++) {
457				var $markerWrap = $('<div>', {
458					'class': 'gmm-marker'
459				}).mouseover({field: this}, function (event) {
460					event.data.field.markers[i].marker.setOptions({
461						icon: pluginData.pluginUrl + 'assets/images/marker-big.png',
462					});
463					acf.doAction('google_map_multi_marker_list_mouseover', event.data.field.markers[i], event.data.field);
464				}).mouseout({field: this}, function (event) {
465					event.data.field.markers[i].marker.setOptions({
466						icon: null
467					});
468					acf.doAction('google_map_multi_marker_list_mouseout', event.data.field.markers[i], event.data.field);
469				});
470
471				var $address = $('<span>', {
472					'class': 'gmm-marker--address',
473				}).text(this.markers[i].values.address).appendTo($markerWrap);
474
475				var $deleteIcon = $('<span>', {
476					'class': 'gmm-marker--delete-icon dashicons dashicons-trash',
477				}).click({field: this}, function (event) {
478					event.data.field.removeMarker(event.data.field.markers[i].marker);
479				}).appendTo($markerWrap);
480
481				acf.doAction('google_map_multi_render_marker_list', $markerWrap, this);
482
483				this.$markerList().append($markerWrap);
484			}
485
486			// Remove any notices from the field.
487			this.removeNotice();
488		},
489
490		searchPosition: function( lat, lng ){
491			// Start Loading.
492			this.setState( 'loading' );
493
494			// Set up the new marker object to contain the values and marker object.
495			this.markers.push({
496				values: {},
497				marker: {}
498			});
499
500			// Query Geocoder.
501			var latLng = { lat: lat, lng: lng };
502			gmmGeocoder.geocode({ location: latLng }, function( results, status ){
503				console.log('geocode callback');
504				// End Loading.
505				this.setState( '' );
506
507				// Status failure.
508				if( status !== 'OK' ) {
509					this.showNotice({
510						text: acf.__('Location not found: %s').replace('%s', status),
511						type: 'warning'
512					});
513
514					// Remove the empty item we just added to this.markers.
515					this.markers.splice(this.markers.length - 1, 1);
516
517					// Re-render the list of markers under the map.
518					this.renderMarkerList();
519
520				// Success.
521				} else {
522					var val = this.parseResult( results[0] );
523
524					// Override lat/lng to match user defined marker location.
525					// Avoids issue where marker "snaps" to nearest result.
526					val.lat = lat;
527					val.lng = lng;
528
529					// Pushing the newest place to the list of values.
530					this.vals.push( val );
531
532					this.markers[this.markers.length - 1].values = val;
533
534					// Re-render the list of markers under the map.
535					this.renderMarkerList();
536
537					// Setting the value of the hidden field to the JSON list of markers.
538					this.val( this.vals );
539				}
540
541			}.bind( this ));
542		},
543
544		searchPlace: function( place ){
545			// Bail early if no place.
546			if( !place ) {
547				return;
548			}
549
550			// Set up the new marker object to contain the values and marker object.
551			this.markers.push({
552				values: {},
553				marker: {}
554			});
555
556			// Selecting from the autocomplete dropdown will return a rich PlaceResult object.
557			// Be sure to over-write the "formatted_address" value with the one displayed to the user for best UX.
558			if( place.geometry ) {
559				place.formatted_address = this.$search().val();
560				var val = this.parseResult( place );
561
562				this.markers[this.markers.length - 1].values = val;
563
564				// Pushing the newest place to the list of values.
565				this.vals.push( val );
566
567				// Setting the value of the hidden field to the JSON list of markers.
568				this.val( this.vals );
569
570				// Searching a custom address will return an empty PlaceResult object.
571			} else if( place.name ) {
572				this.searchAddress( place.name );
573			}
574		},
575
576		searchAddress: function( address ){
577			// Bail early if no address.
578			if( !address ) {
579				return;
580			}
581
582			// Allow "lat,lng" search.
583			var latLng = address.split(',');
584			if( latLng.length == 2 ) {
585				var lat = parseFloat(latLng[0]);
586				var lng = parseFloat(latLng[1]);
587				if( lat && lng ) {
588					return this.searchPosition( lat, lng );
589				}
590			}
591
592			// Start Loading.
593			this.setState( 'loading' );
594
595			// Query Geocoder.
596			gmmGeocoder.geocode({ address: address }, function( results, status ){
597				// End Loading.
598				this.setState( '' );
599
600				// Status failure.
601				if( status !== 'OK' ) {
602					this.showNotice({
603						text: acf.__('Location not found: %s').replace('%s', status),
604						type: 'warning'
605					});
606
607					// Success.
608				} else {
609					var val = this.parseResult( results[0] );
610
611					// Override address data with parameter allowing custom address to be defined in search.
612					val.address = address;
613
614					// Pushing the newest place to the list of values.
615					this.vals.push( val );
616
617					this.markers[this.markers.length - 1].values = val;
618					this.addMarker({lat: val.lat, lng: val.lng});
619
620					// Add the new place just searched to the bounds and fit the map to these new bounds so the map
621					// expands to show the new marker that's just been added.
622					this.map.fitBounds( this.createMapBounds() );
623
624					// Re-render the list of markers under the map.
625					this.renderMarkerList();
626
627					// Setting the value of the hidden field to the JSON list of markers.
628					this.val( this.vals );
629				}
630
631			}.bind( this ));
632		},
633
634		searchLocation: function(){
635			// Check HTML5 geolocation.
636			if( !navigator.geolocation ) {
637				return alert( acf.__('Sorry, this browser does not support geolocation') );
638			}
639
640			// Start Loading.
641			this.setState( 'loading' );
642
643			// Query Geolocation.
644			navigator.geolocation.getCurrentPosition(
645
646				// Success.
647				function( results ){
648
649					// End Loading.
650					this.setState( '' );
651
652					// Search position.
653					var lat = results.coords.latitude;
654					var lng = results.coords.longitude;
655					this.searchPosition( lat, lng );
656
657				}.bind(this),
658
659				// Failure.
660				function( error ){
661					this.setState( '' );
662				}.bind(this)
663			);
664		},
665
666		/**
667		 * parseResult
668		 *
669		 * Returns location data for the given GeocoderResult object.
670		 *
671		 * @date	15/10/19
672		 * @since	5.8.6
673		 *
674		 * @param	object obj A GeocoderResult object.
675		 * @return	object
676		 */
677		parseResult: function( obj ) {
678
679			// Construct basic data.
680			var result = {
681				address: obj.formatted_address,
682				lat: obj.geometry.location.lat(),
683				lng: obj.geometry.location.lng(),
684			};
685
686			// Add zoom level.
687			result.zoom = this.map.getZoom();
688
689			// Add place ID.
690			if( obj.place_id ) {
691				result.place_id = obj.place_id;
692			}
693
694			// Add place name.
695			if( obj.name ) {
696				result.name = obj.name;
697			}
698
699			// Create search map for address component data.
700			var map = {
701				street_number: [ 'street_number' ],
702				street_name: [ 'street_address', 'route' ],
703				city: [ 'locality' ],
704				state: [
705					'administrative_area_level_1',
706					'administrative_area_level_2',
707					'administrative_area_level_3',
708					'administrative_area_level_4',
709					'administrative_area_level_5'
710				],
711				post_code: [ 'postal_code' ],
712				country: [ 'country' ]
713			};
714
715			// Loop over map.
716			for( var k in map ) {
717				var keywords = map[ k ];
718
719				// Loop over address components.
720				for( var i = 0; i < obj.address_components.length; i++ ) {
721					var component = obj.address_components[ i ];
722					var component_type = component.types[0];
723
724					// Look for matching component type.
725					if( keywords.indexOf(component_type) !== -1 ) {
726
727						// Append to result.
728						result[ k ] = component.long_name;
729
730						// Append short version.
731						if( component.long_name !== component.short_name ) {
732							result[ k + '_short' ] = component.short_name;
733						}
734					}
735				}
736			}
737
738			/**
739			 * Filters the parsed result.
740			 *
741			 * @date	18/10/19
742			 * @since	5.8.6
743			 *
744			 * @param	object result The parsed result value.
745			 * @param	object obj The GeocoderResult object.
746			 */
747			return acf.applyFilters('google_map_multi_result', result, obj, this.map, this);
748		},
749
750		onClickClear: function(){
751			this.val( false );
752		},
753
754		onClickLocate: function(){
755			this.searchLocation();
756		},
757
758		onClickSearch: function(){
759			this.searchAddress( this.$search().val() );
760		},
761
762		onFocusSearch: function( e, $el ){
763			this.setState( 'searching' );
764		},
765
766		onBlurSearch: function( e, $el ){
767
768			// Get saved address value.
769			var val = this.val();
770			var address = val ? val.address : '';
771
772			// Remove 'is-searching' if value has not changed.
773			if( $el.val() === address ) {
774				this.setState( 'default' );
775			}
776		},
777
778		onKeyupSearch: function( e, $el ){
779
780			// Clear empty value.
781			if( !$el.val() ) {
782				this.val( false );
783			}
784		},
785
786		// Prevent form from submitting.
787		onKeydownSearch: function( e, $el ){
788			if( e.which == 13 ) {
789				e.preventDefault();
790				$el.blur();
791			}
792		},
793
794		// Center map once made visible.
795		onShow: function(){
796			if( this.map ) {
797				this.setTimeout( this.center );
798			}
799		},
800	});
801
802	acf.registerFieldType( Field );
803
804	// Vars.
805	var gmmLoading = false;
806	var gmmGeocoder = false;
807
808	/**
809	 * gmmWithAPI
810	 *
811	 * Loads the Google Maps API library and triggers callback.
812	 *
813	 * @date	28/3/19
814	 * @since	5.7.14
815	 *
816	 * @param	function callback The callback to excecute.
817	 * @return	void
818	 */
819
820	function gmmWithAPI( callback ) {
821		// Check if geocoder exists.
822		if( gmmGeocoder ) {
823			return callback();
824		}
825
826		// Check if geocoder API exists.
827		if( acf.isset(window, 'google', 'maps', 'Geocoder') ) {
828			gmmGeocoder = new google.maps.Geocoder();
829			return callback();
830		}
831
832		// Check if we're trying to load 1 or more Google Map fields. We need to execute our callback differently
833		// depending on this.
834		let otherMapFields = acf.findFields({
835			type: 'google_map'
836		});
837
838		// Check if the google_map_api_loaded action has been added.
839		if ( !otherMapFields.length ) {
840			// In this case, the there's no other Google Map field used, meaning we need to load the maps API ourselves.
841			acf.addAction( 'google_map_multi_api_loaded', callback );
842
843			// Bail early if already loading API.
844			if( gmmLoading ) {
845				return;
846			}
847
848			// load api
849			var url = acf.get('google_map_api');
850			if( url ) {
851
852				// Set loading status.
853				gmmLoading = true;
854
855				// Load API
856				$.ajax({
857					url: url,
858					dataType: 'script',
859					cache: true,
860					success: function() {
861						gmmGeocoder = new google.maps.Geocoder();
862						acf.doAction('google_map_multi_api_loaded');
863					}
864				});
865			}
866		}
867		else {
868			// The Maps API will be loaded by the original Google Map field.
869			// All we need to do is add our callback to the the action used in the original Google Map field so it gets
870			// triggered together with that.
871			acf.addAction( 'google_map_api_loaded', callback );
872		}
873	}
874
875})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.