homeflowjs 0.12.23 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -243,7 +243,7 @@ export const setSelectedMarker = (payload) => ({
243
243
  payload,
244
244
  });
245
245
 
246
- export const setBreadcrumbs = (payload) => ({
247
- type: PropertiesActionTypes.SET_BREADCRUMBS,
246
+ export const setGeonames = (payload) => ({
247
+ type: PropertiesActionTypes.SET_GEONAMES,
248
248
  payload,
249
249
  });
@@ -8,7 +8,7 @@ const PropertiesActionTypes = {
8
8
  SET_PAGINATION: 'SET_PAGINATION',
9
9
  SET_PROPERTY_LINKS: 'SET_PROPERTY_LINKS',
10
10
  SET_SELECTED_MARKER: 'SET_SELECTED_MARKER',
11
- SET_BREADCRUMBS: 'SET_BREADCRUMBS',
11
+ SET_GEONAMES: 'SET_GEONAMES',
12
12
  };
13
13
 
14
14
  export default PropertiesActionTypes;
@@ -1,4 +1,6 @@
1
1
  const replaceImageDimensions = (src, width = '_', height = '_') => {
2
+ if (!src) return null;
3
+
2
4
  const srcToArray = src.split('/');
3
5
  // /files/photos/12345/6789/my-photo.jpg
4
6
  const srcPathWithoutSize = !isNaN(srcToArray[srcToArray.length - 2])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homeflowjs",
3
- "version": "0.12.23",
3
+ "version": "0.13.0",
4
4
  "sideEffects": [
5
5
  "modal/**/*",
6
6
  "user/default-profile/**/*",
@@ -7,7 +7,6 @@ import { buildQueryString } from '../../search/property-search/property-search';
7
7
  import { setPlace, setSearchField } from '../../actions/search.actions';
8
8
  import { setProperties, setSelectedMarker, setPagination } from '../../actions/properties.actions';
9
9
  import { setLoading } from '../../actions/app.actions';
10
- import { currentGeonameId } from '../../utils/index';
11
10
 
12
11
  const element = function (X, Y) {
13
12
  this.X = X;
@@ -24,21 +23,23 @@ export default class DraggableMap {
24
23
  this.render = this.render.bind(this);
25
24
  this.generateMap = this.generateMap.bind(this);
26
25
  this.properties = this.getProperties();
27
- this.breadcrumbs = null;
28
- if (Homeflow.get('select_marker_on_click')) {
29
- this.selectedMarker = null;
30
- store.subscribe(() => {
31
- const newSelectedMarker = store.getState().properties.selectedMarker;
32
- const newSelectedPropertyID = this.markerPropertyIdOrGeonameId(newSelectedMarker);
33
- const stateSelectedPropertyID = this.markerPropertyIdOrGeonameId(this.selectedMarker);
34
-
35
- if (newSelectedPropertyID !== stateSelectedPropertyID) {
36
- this.activateMarker(newSelectedMarker);
37
- }
38
- })
39
- }
26
+ this.initOnMarkerClick();
27
+ }
28
+
29
+ initOnMarkerClick() {
30
+ if (!Homeflow.get('select_marker_on_click')) return;
31
+
32
+ this.selectedMarker = null;
40
33
 
41
- if (Homeflow.get('breadcrumbs_map')) store.subscribe(() => this.handleBreadcrumbsInit());
34
+ store.subscribe(() => {
35
+ const newSelectedMarker = store.getState().properties.selectedMarker;
36
+ const newSelectedPropertyID = newSelectedMarker?.property?.property_id;
37
+ const stateSelectedPropertyID = this.selectedMarker?.property?.property_id;
38
+
39
+ if (newSelectedMarker && newSelectedPropertyID && newSelectedPropertyID !== stateSelectedPropertyID) {
40
+ this.activateMarker(newSelectedMarker);
41
+ }
42
+ });
42
43
  }
43
44
 
44
45
  getSearch() {
@@ -58,7 +59,7 @@ export default class DraggableMap {
58
59
  this.render();
59
60
  this.buildPolygon();
60
61
  this.setMarkers();
61
- if (this.noLocationfound || Homeflow.get('breadcrumbs_map')) { this.setToMarkeredBounds(); }
62
+ if (this.noLocationfound) { this.setToMarkeredBounds(); }
62
63
  if (Homeflow.get('custom_map_zoom')) {
63
64
  this.map.setZoom(Homeflow.get('custom_map_zoom'));
64
65
  }
@@ -69,11 +70,6 @@ export default class DraggableMap {
69
70
  }
70
71
 
71
72
  render() {
72
- // let lastSearch;
73
- // if (lastSearch = Ctesius.getUserHistoryCollection().last()) {
74
- // Ctesius.storedPlace = lastSearch.get('place');
75
- // }
76
-
77
73
  this.generateMap();
78
74
  if (!Homeflow.get('free_text_search')) {
79
75
  this.map.on('zoomend', () => (this.onMapDrag()));
@@ -92,17 +88,11 @@ export default class DraggableMap {
92
88
  }
93
89
 
94
90
  activateMarker(marker) {
95
- if (this.selectedMarker) {
91
+ if (this.selectedMarker?.property) {
96
92
  this.selectedMarker.setIcon(this.generateMarkerIcon(this.selectedMarker.property));
97
93
  }
98
94
  this.selectedMarker = marker;
99
95
  if (this.selectedMarker) this.selectedMarker.setIcon(this.generateMarkerIcon(marker.property));
100
-
101
- if (marker.property.geoname_id && marker.property.viewport) {
102
- this.bounds = this.getViewportBounds(marker.property.viewport);
103
- this.map.fitBounds(this.bounds, Homeflow.get('custom_map_bounds_padding'));
104
- this.onMapDrag({ markerPlaceId: marker.property.geoname_id });
105
- }
106
96
  }
107
97
 
108
98
  generateMarkerIcon(property) {
@@ -153,6 +143,7 @@ export default class DraggableMap {
153
143
  }
154
144
 
155
145
  marker.on('click', e => Homeflow.kickEvent('map_marker_clicked', e.target));
146
+
156
147
  if (Homeflow.get('select_marker_on_click')) {
157
148
  marker.on('click', e => store.dispatch(setSelectedMarker(e.target)));
158
149
  }
@@ -260,44 +251,29 @@ export default class DraggableMap {
260
251
  const radius = Homeflow.get('custom_clustering_radius') || 10;
261
252
 
262
253
  this.clusteringMarkerLayer = new L.MarkerClusterGroup({ maxClusterRadius: radius, showCoverageOnHover: false });
263
- this.marker_layer = new L.MarkerClusterGroup({ maxClusterRadius: radius, showCoverageOnHover: false });
264
254
  this.nonClusteringMarkerLayer = L.featureGroup();
255
+ this.marker_layer = new L.MarkerClusterGroup({ maxClusterRadius: radius, showCoverageOnHover: false });
265
256
  }
266
257
 
267
258
  setMarkers() {
268
259
  this.initLayers();
269
260
 
270
- if (!this.properties && !this.breadcrumbs) { return null }
271
-
272
-
273
- if (this.isDisplayProperties()) {
274
- this.properties?.map(property => this.setPropertyMarker(property));
275
- } else {
276
- this.breadcrumbs?.map(breadcrumb => this.setBreadcrumbMarker(breadcrumb));
277
- }
261
+ if (!this.properties) { return null }
278
262
 
263
+ this.properties.map(property => this.setPropertyMarker(property));
279
264
  this.clusteringMarkerLayer.addTo(this.map);
280
- this.nonClusteringMarkerLayer.addTo(this.map);
281
265
 
282
266
  return this.bounds;
283
267
  }
284
268
 
285
269
  setPropertyMarker(property) {
286
- const geonameId = property.geoname_ids?.slice(-1)[0];
287
270
  const layer = Homeflow.get('pin_clustering') ? this.clusteringMarkerLayer : this.nonClusteringMarkerLayer;
288
271
 
289
272
  if (property.property_id === null || property.lat === 0 || property.lng === 0) return;
290
- if (geonameId && Homeflow.get('breadcrumbs_map') && geonameId !== currentGeonameId() && !this.isDisplayProperties()) return;
291
273
 
292
274
  layer.addLayer(this.generateMarker(property));
293
275
  }
294
276
 
295
- setBreadcrumbMarker(breadcrumb) {
296
- if (!breadcrumb.lat || !breadcrumb.lng) return;
297
-
298
- this.nonClusteringMarkerLayer.addLayer(this.generateMarker(breadcrumb));
299
- }
300
-
301
277
  setToMarkeredBounds() {
302
278
  const viewport = store.getState().search.currentSearch.place?.viewport;
303
279
 
@@ -323,17 +299,10 @@ export default class DraggableMap {
323
299
  if (this.bounds && this.bounds.isValid()) {
324
300
  return this.bounds;
325
301
  } else {
326
- if (Homeflow.get('breadcrumbs_map') && this.viewport) {
327
- const viewportBounds = this.getViewportBounds();
328
- if (viewportBounds.flat().find(coordinate => coordinate === Infinity)) return null;
329
-
330
- return L.latLngBounds(...viewportBounds.map(bound => L.latLng(...bound)));
331
- } else {
332
- const bounds = [this.getTopRightMarkerCoordinates(), this.getBottomLeftMarkerCoordinates()];
333
- if (bounds.flat().find(coordinate => coordinate === Infinity)) return null;
302
+ const bounds = [this.getTopRightMarkerCoordinates(), this.getBottomLeftMarkerCoordinates()];
303
+ if (bounds.flat().find(coordinate => coordinate === Infinity)) return null;
334
304
 
335
- return L.latLngBounds(...bounds.map(bound => L.latLng(...bound)));
336
- }
305
+ return L.latLngBounds(...bounds.map(bound => L.latLng(...bound)));
337
306
  }
338
307
  }
339
308
 
@@ -344,15 +313,15 @@ export default class DraggableMap {
344
313
 
345
314
  getTopRightMarkerCoordinates() {
346
315
  return [
347
- Math.max(...this.parseCoordinateArray([this.properties, this.breadcrumbs].flat(), 'lat')),
348
- Math.max(...this.parseCoordinateArray([this.properties, this.breadcrumbs].flat(), 'lng'))
316
+ Math.max(...this.parseCoordinateArray(this.properties, 'lat')),
317
+ Math.max(...this.parseCoordinateArray(this.properties, 'lng'))
349
318
  ];
350
319
  }
351
320
 
352
321
  getBottomLeftMarkerCoordinates() {
353
322
  return [
354
- Math.min(...this.parseCoordinateArray([this.properties, this.breadcrumbs].flat(), 'lat')),
355
- Math.min(...this.parseCoordinateArray([this.properties, this.breadcrumbs].flat(), 'lng'))
323
+ Math.min(...this.parseCoordinateArray(this.properties, 'lat')),
324
+ Math.min(...this.parseCoordinateArray(this.properties, 'lng'))
356
325
  ];
357
326
  }
358
327
 
@@ -362,18 +331,6 @@ export default class DraggableMap {
362
331
  return items.filter(item => item).map(item => parseFloat(item[coordinateType])).filter(Number);
363
332
  }
364
333
 
365
- isDisplayProperties() {
366
- const { properties: { pagination: { total_count: totalCount } } } = store.getState();
367
-
368
- if (!this.breadcrumbs || this.breadcrumbs?.length === 0 || !Homeflow.get('breadcrumbs_map')) {
369
- return true;
370
- } else if (this.properties && totalCount <= this.properties.length) {
371
- return true;
372
- } else {
373
- return false;
374
- }
375
- }
376
-
377
334
  buildSubPolygon(polygon, extraPolygon, style = null) {
378
335
  let bounds = [];
379
336
  const points = [];
@@ -494,32 +451,25 @@ export default class DraggableMap {
494
451
  Homeflow.kickEvent('before_draggable_map_updated');
495
452
  url = url + '&zoom_level=' + this.map.getZoom();
496
453
 
497
- if (markerPlaceId) {
498
- url = url + `&marker_place_id=${markerPlaceId}`;
499
- }
500
-
501
454
  return fetch(url)
502
455
  .then((response) => response.json())
503
456
  .then(json => {
504
457
  Homeflow.kickEvent('draggable_map_updated', json);
505
- store.dispatch(setProperties(json.properties))
506
458
  this._running_update = false;
507
-
508
- if (json.breadcrumbs) {
509
- this.breadcrumbs = this.concatenateDistinctObjects('geoname_id', this.breadcrumbs, json.breadcrumbs);
510
- this.properties = [];
511
- } else if (json.properties) {
512
- this.properties = this.concatenateDistinctObjects('property_id', this.properties, json.properties);
513
- this.breadcrumbs = [];
514
- }
515
- // TODO: figure out what 'tile view' is
516
- // if (this.tile_view != null) {
517
- // this.tile_view = new Ctesius.Views.Tiles({ collection: this.collection });
518
- // }
519
- return this.setMarkers();
459
+ this.setSearchResponse(json);
520
460
  });
521
461
  }
522
462
 
463
+ setSearchResponse(json) {
464
+ store.dispatch(setProperties(json.properties));
465
+
466
+ if (json.properties) {
467
+ this.properties = json.properties;
468
+ }
469
+
470
+ this.setMarkers();
471
+ }
472
+
523
473
  // this method was copied directly from transpiled ES5 from the browser as it's the only
524
474
  // way I could get it to work.
525
475
  buildDrawnPolygon(layer) {
@@ -654,49 +604,20 @@ export default class DraggableMap {
654
604
 
655
605
  Homeflow.kickEvent('before_draggable_map_updated'); //use to show some loading?
656
606
 
657
- // if (Homeflow.get('get_geo_features')) {
658
- // $.get(geo_url, (res, status, xhr) => {
659
- // this._running_update = false;
660
- // if (this.geo_marker_layer != null) { this.map.removeLayer(this.geo_marker_layer); }
661
- // return this.addGeoMarkers(res);
662
- // });
663
- // }
664
-
665
- // TODO: Rewrite this to use fetch() and update redux store
666
607
  fetch(url)
667
608
  .then((response) => response.json())
668
609
  .then((json) => {
669
- store.dispatch(setProperties(json.properties));
670
610
  store.dispatch(setPagination(json.pagination))
671
611
  // TODO: implement user history
672
612
  // Ctesius.getUserHistoryCollection().addSearch(s);
673
613
  Homeflow.kickEvent('clear_search_box');
674
614
  Homeflow.kickEvent('draggable_map_updated', json);
675
615
  this._running_update = false;
676
- this.properties = json.properties;
677
616
 
678
- // if (this.tile_view != null) {
679
- // this.tile_view = new Ctesius.Views.Tiles({ collection: this.collection });
680
- // }
681
-
682
- return this.setMarkers();
617
+ if (json.properties) {
618
+ setSearchResponse(json);
619
+ }
683
620
  })
684
- // return $.get(url, (res, status, xhr) => {
685
- // s.set('performed_data', res);
686
- // Ctesius.getUserHistoryCollection().addSearch(s);
687
- // Homeflow.kickEvent('clear_search_box');
688
- // Homeflow.kickEvent('draggable_map_updated', res);
689
- // this._running_update = false;
690
- // if (this.marker_layer != null) { this.map.removeLayer(this.marker_layer); }
691
- // this.collection = new Ctesius.Collections.Properties(res.properties);
692
- // Ctesius.getPropertiesCollection().add(res.properties);
693
- // if (this.tile_view != null) {
694
- // this.tile_view = new Ctesius.Views.Tiles({ collection: this.collection });
695
- // }
696
- // else { }
697
- // //@tile_view.collection(@collection)
698
- // return this.setMarkers();
699
- // });
700
621
  }
701
622
 
702
623
 
@@ -729,30 +650,4 @@ export default class DraggableMap {
729
650
 
730
651
  return new L.LatLng(y, x);
731
652
  }
732
-
733
- handleBreadcrumbsInit() {
734
- if (this.breadcrumbsInitialized) return;
735
-
736
- const { properties: { breadcrumbs } } = store.getState();
737
-
738
- if (breadcrumbs && this.breadcrumbs !== breadcrumbs) {
739
- this.breadcrumbs = breadcrumbs;
740
- this.setMarkers();
741
- this.setToMarkeredBounds();
742
- }
743
-
744
- this.breadcrumbsInitialized = true;
745
- }
746
-
747
- concatenateDistinctObjects(key, array, otherArray) {
748
- return [...new Map((array || []).concat(otherArray || []).map((object) => [object[key], object])).values()];
749
- }
750
-
751
- markerPropertyIdOrGeonameId(marker) {
752
- if (!marker) return null;
753
- if (marker?.property?.property_id) return marker.property.property_id;
754
- if (marker?.property?.geoname_id) return marker.property.geoname_id;
755
-
756
- return null;
757
- }
758
653
  }
@@ -22,9 +22,7 @@ export default class DrawableMap extends DraggableMap {
22
22
  this.repositionDrawControls();
23
23
  // note: I'm not 100% sure this is will ever be required if super.init() is called
24
24
  // something to review in homeflow_next
25
- if (!Homeflow.get('breadcrumbs_map')) {
26
- return this.onMapDrag();
27
- }
25
+ this.onMapDrag();
28
26
  }
29
27
 
30
28
  onMapDrag({ markerPlaceId } = { markerPlaceId: null }) {
@@ -0,0 +1,415 @@
1
+ /* eslint-disable */
2
+ import DraggableMap from './draggable-map';
3
+ import store from '../../store';
4
+ import { currentGeonameId } from '../../utils/index';
5
+ import { setProperties, setGeonames, setSelectedMarker } from '../../actions/properties.actions';
6
+ import { setInitialSearch, setPlace, setSearchField } from '../../actions/search.actions';
7
+ import { buildQueryString } from '../../search/property-search/property-search';
8
+ import mapLoader from './map-loader';
9
+ import './geonames.css';
10
+
11
+ export default class GeonamesMap extends DraggableMap {
12
+ constructor() {
13
+ super();
14
+
15
+ if (!Homeflow.get('custom_map_zoom_location')) Homeflow.set('custom_map_zoom_location', 'topright');
16
+
17
+ this.geonameMarkerLayers = [];
18
+ this.geonameMarkerHoverLayers = [];
19
+ }
20
+
21
+ initOnMarkerClick() {
22
+ super.initOnMarkerClick();
23
+
24
+ store.subscribe(() => {
25
+ const newSelectedMarker = store.getState().properties.selectedMarker;
26
+ const newSelectedGeonameID = newSelectedMarker?.geoname?.geoname_id;
27
+ const stateSelectedGeonameID = this.selectedMarker?.geoname?.geoname_id;
28
+
29
+ if (newSelectedGeonameID && newSelectedGeonameID !== stateSelectedGeonameID) {
30
+ this.activateGeonameMarker(newSelectedMarker);
31
+ }
32
+ });
33
+ }
34
+
35
+ init() {
36
+ this.element = Homeflow.get('draggable_map_view');
37
+ this.renderLoader();
38
+ this.showLoader();
39
+ this.render();
40
+ this.geonamesInit();
41
+
42
+ /**
43
+ * The map tries to render once before the geonames exist in Redux
44
+ * We must wait for these to be populated before initializing the
45
+ * geonames again, then unsubscribe.
46
+ */
47
+ const prevGeonames = store.getState().properties.geonames;
48
+ const prevProperties = store.getState().properties.properties;
49
+
50
+ this.unsubscribeFromGeonamesChanges = store.subscribe(() => {
51
+ const { geonames, properties } = store.getState().properties
52
+
53
+ if (properties?.length) this.geonamesInit();
54
+
55
+ if (prevGeonames === undefined && geonames !== undefined) {
56
+ this.geonamesInit();
57
+ this.unsubscribeFromGeonamesChanges();
58
+ }
59
+ });
60
+
61
+ this.map.on('zoomend', () => {
62
+ this.setMarkerTypeforZoomLevel()
63
+
64
+ this.setMarkers();
65
+ });
66
+
67
+ /**
68
+ * when the user drags the map, we need to clear the place ID so subsequent
69
+ * searches are not restricted to that place ID. We also clear the query so
70
+ * it's clear to the user this is no longer a search for that location.
71
+ */
72
+ this.map.on('dragend', () => {
73
+ store.dispatch(setPlace(null))
74
+ store.dispatch(setSearchField({ q: '' }));
75
+ });
76
+
77
+ /**
78
+ * If for some reason the markers have not rendered after a few seconds
79
+ * render them so we don't have an empty map
80
+ */
81
+ setTimeout(() => {
82
+ if (!this.markersInitialized) {
83
+ this.setMarkerTypeforZoomLevel();
84
+ this.setMarkers();
85
+ this.setToMarkeredBounds();
86
+ }
87
+ }, 5000);
88
+ }
89
+
90
+ setMarkerTypeforZoomLevel() {
91
+ let zoomLevel = this.map.getZoom();
92
+
93
+ if (!zoomLevel) zoomLevel = window.lastZoomLevel;
94
+
95
+ if (zoomLevel > 9) {
96
+ window.markerType = 'properties';
97
+ } else {
98
+ window.markerType = 'geonames';
99
+ }
100
+
101
+ window.lastZoomLevel = zoomLevel
102
+ }
103
+
104
+ renderLoader() {
105
+ const loader = document.getElementById('geonames-map-loader');
106
+ if (loader) return loader.remove();
107
+
108
+ document.querySelector('.js-geo-map-wrapper')?.insertAdjacentHTML('beforeend', mapLoader);
109
+ }
110
+
111
+ showLoader() {
112
+ const loader = document.getElementById('geonames-map-loader');
113
+ loader?.classList.remove('hidden');
114
+ }
115
+
116
+ hideLoader() {
117
+ const loader = document.getElementById('geonames-map-loader');
118
+ loader?.classList.add('hidden');
119
+ }
120
+
121
+ setToMarkeredBounds() {
122
+ return super.setToMarkeredBounds();
123
+ }
124
+
125
+ getViewportBounds(viewport) {
126
+ return super.getViewportBounds(viewport);
127
+ }
128
+
129
+ generateMarkerIcon(propertyOrGeoname) {
130
+ return super.generateMarkerIcon(propertyOrGeoname);
131
+ }
132
+
133
+ initLayers() {
134
+ const maxClusterRadius = Homeflow.get('custom_clustering_radius') || 10;
135
+
136
+ if (this.propertiesLayer) this.map.removeLayer(this.propertiesLayer);
137
+ if (this.geonamesLayer) this.map.removeLayer(this.geonamesLayer);
138
+
139
+ this.propertiesLayer = new L.MarkerClusterGroup({ maxClusterRadius, showCoverageOnHover: false });
140
+ this.geonamesLayer = new L.FeatureGroup();
141
+ }
142
+
143
+ hideSuggestedDestinations() {
144
+ const el = document.querySelector('.suggested-destinations');
145
+
146
+ if (el) el.style.display = 'none';
147
+ }
148
+
149
+ showSuggestedDestinations() {
150
+ const el = document.querySelector('.suggested-destinations');
151
+
152
+ if (el) el.style.display = 'block';
153
+ }
154
+
155
+ geonamesInit() {
156
+ if (!this.map || this.geonamesInitialized) return;
157
+
158
+ const { geonames } = store.getState().properties;
159
+
160
+ if (geonames && !this.geonames) {
161
+ this.geonamesInitialized = true;
162
+ this.geonames = geonames;
163
+ }
164
+
165
+ this.setMarkers();
166
+ this.setToMarkeredBounds();
167
+
168
+ const zoomLevel = this.map.getZoom();
169
+
170
+ // If we don't have any geonames we need to show property markers
171
+ if (!geonames && zoomLevel > 9) window.markerType = 'properties';
172
+
173
+ /**
174
+ * If we don't have any geonames we need to run onMapDrag
175
+ * as it doesn't run for open searches otherwise and we need
176
+ * it to load geonames
177
+ */
178
+ if (!geonames && zoomLevel < 10) {
179
+ this.onMapDrag();
180
+ window.markerType = 'geonames';
181
+ }
182
+ }
183
+
184
+ getMarkerBounds() {
185
+ if (this.viewport) {
186
+ const viewportBounds = this.getViewportBounds();
187
+ if (viewportBounds.flat().find(coordinate => coordinate === Infinity)) return null;
188
+
189
+ return L.latLngBounds(...viewportBounds.map(bound => L.latLng(...bound)));
190
+ } else {
191
+ return super.getMarkerBounds();
192
+ }
193
+ }
194
+
195
+ getTopRightMarkerCoordinates() {
196
+ return [
197
+ Math.max(...this.parseCoordinateArray([this.properties, this.geonames].flat(), 'lat')),
198
+ Math.max(...this.parseCoordinateArray([this.properties, this.geonames].flat(), 'lng'))
199
+ ];
200
+ }
201
+
202
+ getBottomLeftMarkerCoordinates() {
203
+ return [
204
+ Math.min(...this.parseCoordinateArray([this.properties, this.geonames].flat(), 'lat')),
205
+ Math.min(...this.parseCoordinateArray([this.properties, this.geonames].flat(), 'lng'))
206
+ ];
207
+ }
208
+
209
+ setSearchResponse(json) {
210
+ if (!json) return;
211
+
212
+ if (json.geonames) {
213
+ this.geonames = this.concatenateDistinctObjects('geoname_id', this.geonames, json.geonames);
214
+ window.markerType = 'geonames';
215
+ store.dispatch(setGeonames(json.geonames));
216
+ } else if (json.properties) {
217
+ this.properties = this.concatenateDistinctObjects('property_id', this.properties, json.properties);
218
+ window.markerType = 'properties';
219
+ store.dispatch(setProperties(json.properties));
220
+ }
221
+
222
+ if (json.location) {
223
+ store.dispatch(setPlace(json.location));
224
+ store.dispatch(setInitialSearch({ q: json.location.name }));
225
+ }
226
+
227
+ this.setMarkers();
228
+ }
229
+
230
+ setMarkers() {
231
+ this.initLayers();
232
+
233
+ if (window.markerType === 'properties' && this.properties?.length) {
234
+ this.properties.forEach(property => this.setPropertyMarker(property));
235
+ this.hideSuggestedDestinations();
236
+ this.markersInitialized = true;
237
+ } else if (window.markerType === 'geonames' && this.geonames?.length) {
238
+ this.geonames.forEach(geoname => this.setGeonameMarker(geoname));
239
+ this.showSuggestedDestinations();
240
+ this.markersInitialized = true;
241
+ }
242
+
243
+ this.geonamesLayer.addTo(this.map);
244
+ this.propertiesLayer.addTo(this.map);
245
+ }
246
+
247
+ generateMarker(propertyOrGeoname) {
248
+ const marker = L.marker(
249
+ [propertyOrGeoname.lat, propertyOrGeoname.lng],
250
+ { title: propertyOrGeoname.name, icon: this.generateMarkerIcon(propertyOrGeoname) }
251
+ );
252
+
253
+ marker.on('click', () => {
254
+ if (marker.geoname) {
255
+ this.geonameMarkerClick(marker);
256
+ store.dispatch(setSelectedMarker(marker));
257
+ } else if (marker.property) {
258
+ this.propertyMarkerClick(marker);
259
+ store.dispatch(setSelectedMarker(marker));
260
+ }
261
+ });
262
+
263
+ if (Homeflow.get('show_geoname_polygon_on_mouseover') && propertyOrGeoname.polygon?.length) {
264
+ marker.on('mouseover', () => this.geonameMarkerMouseOver(marker));
265
+ marker.on('mouseout', () => this.geonameMarkerMouseOut(marker));
266
+ }
267
+
268
+ if (propertyOrGeoname.geoname_id) {
269
+ marker.geoname = propertyOrGeoname;
270
+ } else {
271
+ marker.property = propertyOrGeoname;
272
+ }
273
+
274
+ return marker;
275
+ }
276
+
277
+ geonameMarkerMouseOver(marker) {
278
+ const selectedGeonameId = this.selectedMarker?.geoname?.geoname_id;
279
+
280
+ if (selectedGeonameId && selectedGeonameId === marker.geoname.geoname_id) return;
281
+
282
+ this.showGeonameMarkerPolygon(
283
+ { polygon: marker.geoname.polygon, layers: this.geonameMarkerHoverLayers }
284
+ );
285
+ };
286
+
287
+ geonameMarkerMouseOut(marker) {
288
+ this.hideGeonameMarkerPolygon({ layers: this.geonameMarkerHoverLayers });
289
+ }
290
+
291
+ setPropertyMarker(property) {
292
+ this.propertiesLayer.addLayer(this.generateMarker(property));
293
+ }
294
+
295
+ setGeonameMarker(geoname) {
296
+ if (!geoname.lat || !geoname.lng) return;
297
+
298
+ this.geonamesLayer.addLayer(this.generateMarker(geoname));
299
+ }
300
+
301
+ activateGeonameMarker(marker) {
302
+ this.selectedMarker = marker;
303
+ this.bounds = this.getViewportBounds(marker.geoname.viewport);
304
+ this.map.fitBounds(this.bounds, Homeflow.get('custom_map_bounds_padding'));
305
+
306
+ this.onMapDrag({ markerPlaceId: marker.geoname.geoname_id });
307
+ }
308
+
309
+ propertyMarkerClick(marker) {
310
+ const { properties } = store.getState().properties;
311
+
312
+ // Ensure active property is included in list view before trying to select it.
313
+ store.dispatch(
314
+ setProperties(this.concatenateDistinctObjects('property_id', properties, [marker.property]))
315
+ );
316
+ }
317
+
318
+ geonameMarkerClick(marker) {
319
+ this.showLoader();
320
+ if (!marker.geoname) return;
321
+
322
+ if (marker.geoname.polygon?.length) {
323
+ this.hideGeonameMarkerPolygon({ layers: this.geonameMarkerHoverLayers });
324
+ this.hideGeonameMarkerPolygon({ layers: this.geonameMarkerLayers });
325
+ this.showGeonameMarkerPolygon({ polygon: marker.geoname.polygon, layers: this.geonameMarkerLayers });
326
+ }
327
+
328
+ // If a location marker is clicked, allow updating search results count.
329
+ Homeflow.set('location_property_count', marker.geoname.properties_count);
330
+
331
+ // If user clicks a marker and no more markers are visible, they may expect something to
332
+ // hapen if they click the marker again. Zoom in one step to give a sense of feedback.
333
+ if (this.lastClickedGeonameId === marker.geoname.geoname_id) {
334
+ const center = new L.LatLng(marker.geoname.lat, marker.geoname.lng);
335
+ this.map.setView(center, this.map.getZoom() + 1);
336
+ } else {
337
+ this.lastClickedGeonameId = marker.geoname.geoname_id;
338
+ }
339
+ }
340
+
341
+ onMapDrag({ markerPlaceId } = { markerPlaceId: null }) {
342
+ this.showLoader();
343
+
344
+ this.mostRecentRequestId = crypto.randomUUID();
345
+
346
+ const bounds = this.getSearchableBounds();
347
+ const zoomLevel = this.map.getZoom();
348
+ const params = [
349
+ `${buildQueryString(this.getSearch())}/view-${bounds.toBBoxString()}`,
350
+ '&count=50',
351
+ `&zoom_level=${zoomLevel}`,
352
+ `&request_id=${this.mostRecentRequestId}`,
353
+ ];
354
+
355
+ if (markerPlaceId) {
356
+ params.push(`&marker_place_id=${markerPlaceId}`);
357
+ }
358
+
359
+ const url = `/search.ljson?${params.join('')}`;
360
+
361
+ /**
362
+ * If the map has already loaded with a location we don't want it to
363
+ * make another request for the viewport as it will load properties
364
+ * outside the polygon so just remove the loader and return
365
+ */
366
+ if (!this.mapRenderedOnce) {
367
+ this.hideLoader();
368
+ return this.mapRenderedOnce = true;
369
+ }
370
+
371
+ fetch(url)
372
+ .then((response) => this.processResponse(response))
373
+ .then((json) => this.setSearchResponse(json))
374
+ .finally(() => this.hideLoader())
375
+ .catch((err) => console.error(err));
376
+ }
377
+
378
+ processResponse(response) {
379
+ if (response.headers.get('X-Homeflow-Request-Id') === this.mostRecentRequestId) {
380
+ return response.json();
381
+ } else {
382
+ return Promise.resolve(null);
383
+ }
384
+ }
385
+
386
+ hideGeonameMarkerPolygon({ layers }) {
387
+ if (!layers?.length) return;
388
+
389
+ layers.forEach((layer) => this.map.removeLayer(layer));
390
+ layers.length = 0;
391
+ }
392
+
393
+ showGeonameMarkerPolygon({ polygon, layers }) {
394
+ const shapeStyle = {
395
+ clickable: false,
396
+ fill: true,
397
+ color: Homeflow.get('polygon_color', '#3388ff'),
398
+ fillColor: Homeflow.get('polygon_fill_color', null),
399
+ fillOpacity: .2,
400
+ opacity: .5,
401
+ stroke: true,
402
+ weight: 4,
403
+ };
404
+
405
+ polygon.forEach((coordinates) => {
406
+ layers.push(new L.Polygon(coordinates.map(longLat => longLat.toReversed()), shapeStyle));
407
+ });
408
+
409
+ layers.forEach((layer) => this.map.addLayer(layer));
410
+ }
411
+
412
+ concatenateDistinctObjects (key, array, otherArray) {
413
+ return [...new Map((array || []).concat(otherArray || []).map((object) => [object[key], object])).values()];
414
+ }
415
+ }
@@ -0,0 +1,40 @@
1
+ .geonames-map__loader-background {
2
+ position: absolute;
3
+ top: 0;
4
+ left: 0;
5
+ width: 100%;
6
+ bottom: 51px;
7
+ z-index: 200;
8
+ pointer-events: none;
9
+ }
10
+
11
+ .geonames-map__loader-inner {
12
+ position: relative;
13
+ top: 50%;
14
+ transform: translateY(-50%);
15
+ text-align: center;
16
+ font-weight: bold;
17
+ font-size: 2rem;
18
+ }
19
+
20
+ .geonames-map__loader.hidden {
21
+ display: none;
22
+ }
23
+
24
+ @media (min-width: 1024px) {
25
+ .geonames-map__loader-background {
26
+ top: 240px;
27
+ left: 43px;
28
+ width: 59%;
29
+ }
30
+ }
31
+
32
+ @media (min-width: 1400px) {
33
+ .geonames-map__loader-background {
34
+ left: 10%;
35
+ }
36
+ }
37
+
38
+ .map-cluster-marker, .map-cluster-marker svg, .map-cluster-marker path, .map-cluster-marker__count {
39
+ pointer-events: none;
40
+ }
@@ -0,0 +1,23 @@
1
+ export default `
2
+ <div id="geonames-map-loader" class="js-geonames-map-loader geonames-map__loader-background">
3
+ <div class="geonames-map__loader-inner">
4
+ <svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
5
+ <g fill="none" fill-rule="evenodd">
6
+ <g transform="translate(1 1)" stroke-width="2">
7
+ <circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
8
+ <path d="M36 18c0-9.94-8.06-18-18-18">
9
+ <animateTransform
10
+ attributeName="transform"
11
+ type="rotate"
12
+ from="0 18 18"
13
+ to="360 18 18"
14
+ dur="1s"
15
+ repeatCount="indefinite"
16
+ />
17
+ </path>
18
+ </g>
19
+ </g>
20
+ </svg>
21
+ </div>
22
+ </div>
23
+ `;
@@ -8,6 +8,7 @@ import PropTypes from 'prop-types';
8
8
  // TODO: Possibly lazy-load these
9
9
  import DraggableMap from './draggable-map';
10
10
  import DrawableMap from './drawable-map';
11
+ import GeonamesMap from './geonames-map';
11
12
 
12
13
  import './leaflet.css';
13
14
  import './leaflet.draw.css';
@@ -16,7 +17,10 @@ import './marker_cluster.default.css';
16
17
 
17
18
  // global callback to run when maps scripts are loaded
18
19
  window.initLegacyMap = () => {
19
- if (Homeflow.get('enable_draw_a_map') && !!window.CanvasRenderingContext2D && !!history.pushState) {
20
+ if (Homeflow.get('geonames_map')) {
21
+ const map = new GeonamesMap();
22
+ map.init();
23
+ } else if (Homeflow.get('enable_draw_a_map') && !!window.CanvasRenderingContext2D && !!history.pushState) {
20
24
  const map = new DrawableMap();
21
25
  map.init();
22
26
  } else {
@@ -18,6 +18,7 @@ const SavePropertyButton = (props) => {
18
18
  style,
19
19
  notificationMessage,
20
20
  notifyConfig,
21
+ asAnchorTag,
21
22
  } = props;
22
23
 
23
24
  const isSaved = !!savedProperties.find((p) => p.property_id === parseInt(propertyId, 10));
@@ -36,6 +37,19 @@ const SavePropertyButton = (props) => {
36
37
  }
37
38
  };
38
39
 
40
+ if (asAnchorTag) {
41
+ return (
42
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/anchor-is-valid
43
+ <a
44
+ style={style}
45
+ onClick={toggleProperty}
46
+ className={`${className} ${isSaved ? 'saved' : ''}`}
47
+ >
48
+ {isSaved ? SavedComponent : UnsavedComponent}
49
+ </a>
50
+ );
51
+ }
52
+
39
53
  return (
40
54
  // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/anchor-is-valid
41
55
  <button
@@ -61,6 +75,7 @@ SavePropertyButton.propTypes = {
61
75
  addSavedProperty: PropTypes.func.isRequired,
62
76
  removeSavedProperty: PropTypes.func.isRequired,
63
77
  notifyConfig: PropTypes.object,
78
+ asAnchorTag: PropTypes.bool,
64
79
  };
65
80
 
66
81
  SavePropertyButton.defaultProps = {
@@ -69,6 +84,7 @@ SavePropertyButton.defaultProps = {
69
84
  className: '',
70
85
  style: {},
71
86
  notifyConfig: {},
87
+ asAnchorTag: false,
72
88
  };
73
89
 
74
90
  const mapStateToProps = (state) => ({
@@ -2,6 +2,7 @@ import PropertiesActionTypes from '../actions/properties.types';
2
2
 
3
3
  const INITIAL_STATE = {
4
4
  properties: [],
5
+ geonames: undefined,
5
6
  savedProperties: [],
6
7
  pagination: {},
7
8
  selectedMarker: null,
@@ -83,10 +84,10 @@ const propertiesReducer = (state = INITIAL_STATE, action) => {
83
84
  ...state,
84
85
  selectedMarker: action.payload,
85
86
  };
86
- case 'SET_BREADCRUMBS': {
87
+ case 'SET_GEONAMES': {
87
88
  return {
88
89
  ...state,
89
- breadcrumbs: action.payload,
90
+ geonames: action.payload,
90
91
  }
91
92
  };
92
93
  default:
@@ -60,11 +60,11 @@ describe('propertiesReducer', () => {
60
60
  expect(reducedState).toMatchObject(state);
61
61
  });
62
62
 
63
- it('Sets breadcrumbs when it receives the SET_BREADCRUMBS action', () => {
64
- const breadcrumbs = [{ geoname_id: '12345' }, { geoname_id: '45678' }];
63
+ it('Sets geonames when it receives the SET_GEONAMES action', () => {
64
+ const geonames = [{ geoname_id: '12345' }, { geoname_id: '45678' }];
65
65
  const state = { properties: [] };
66
- const reducedState = propertiesReducer(state, { type: 'SET_BREADCRUMBS', payload: breadcrumbs });
66
+ const reducedState = propertiesReducer(state, { type: 'SET_GEONAMES', payload: geonames });
67
67
 
68
- expect(reducedState.breadcrumbs).toEqual([{ geoname_id: '12345' }, { geoname_id: '45678' }]);
68
+ expect(reducedState.geonames).toEqual([{ geoname_id: '12345' }, { geoname_id: '45678' }]);
69
69
  });
70
70
  });
@@ -50,6 +50,11 @@ export const buildQueryString = (search) => {
50
50
  }
51
51
  });
52
52
 
53
+ // If we're on a map, redirect back to it
54
+ if (window.location.hash === '#/map') {
55
+ queryParams.push('hashbang=map');
56
+ }
57
+
53
58
  let queryString = queryParams.join('&');
54
59
  queryString += `&fragment=${fragmentParams.join('/')}`;
55
60
  return queryString;
@@ -0,0 +1,16 @@
1
+ <svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
2
+ <g fill="none" fill-rule="evenodd">
3
+ <g transform="translate(1 1)" stroke-width="2">
4
+ <circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
5
+ <path d="M36 18c0-9.94-8.06-18-18-18">
6
+ <animateTransform
7
+ attributeName="transform"
8
+ type="rotate"
9
+ from="0 18 18"
10
+ to="360 18 18"
11
+ dur="1s"
12
+ repeatCount="indefinite"/>
13
+ </path>
14
+ </g>
15
+ </g>
16
+ </svg>