homeflowjs 1.0.13 → 1.0.15

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homeflowjs",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "sideEffects": [
5
5
  "modal/**/*",
6
6
  "user/default-profile/**/*",
@@ -21,6 +21,10 @@ const PropertiesDisplay = ({
21
21
  previousBtnClasses,
22
22
  ...other
23
23
  }) => {
24
+ if (!properties?.length) {
25
+ return noResultsMessage;
26
+ }
27
+
24
28
  const propertiesPagination = useSelector((state) => state.properties?.pagination);
25
29
  const infiniteScrollRef = useRef();
26
30
  const {
@@ -53,10 +57,6 @@ const PropertiesDisplay = ({
53
57
  };
54
58
  }, [properties]);
55
59
 
56
- if (!properties?.length) {
57
- return noResultsMessage;
58
- }
59
-
60
60
  const addWrapper = displayType === 'list';
61
61
 
62
62
  const showPreviousBtn = includePreviousBtn && hasPreviousPage && !loadingPreviousProperties;
@@ -18,7 +18,7 @@ const element = function (X, Y) {
18
18
  // TODO: subscribe to store for when properties change and refresh the map
19
19
 
20
20
  export default class DraggableMap {
21
- constructor() {
21
+ constructor({ updateMapOnPropertiesLoadMore, dragWithoutUpdate } = {}) {
22
22
  this.updateURL = this.updateURL.bind(this);
23
23
  this.onPolygonDrawn = this.onPolygonDrawn.bind(this);
24
24
  this.init = this.init.bind(this);
@@ -31,9 +31,10 @@ export default class DraggableMap {
31
31
  this.removeOverlay = this.removeOverlay.bind(this);
32
32
  this.showOverlay = this.showOverlay.bind(this);
33
33
  this.hideOverlay = this.hideOverlay.bind(this);
34
+ this.updateMapOnPropertiesLoadMore = updateMapOnPropertiesLoadMore || false;
35
+ this.dragWithoutUpdate = dragWithoutUpdate || false;
34
36
  this.initOnMarkerClick();
35
37
  }
36
-
37
38
  initOnMarkerClick() {
38
39
  if (!Homeflow.get('select_marker_on_click')) return;
39
40
 
@@ -59,8 +60,26 @@ export default class DraggableMap {
59
60
  return store.getState().properties.properties;
60
61
  }
61
62
 
63
+ /*
64
+ * theme needs both Homeflow.kickEvent('performed_previous_infinite_scroll')
65
+ * and Homeflow.kickEvent('preformed_next_infinite_scroll')
66
+ * to kick the map updates
67
+ */
68
+ initMapUpdateOnPropertiesLoadMore() {
69
+ Homeflow.registerEvent('performed_previous_infinite_scroll', () => {
70
+ this.properties = this.getProperties();
71
+ this.setMarkers();
72
+ });
73
+
74
+ Homeflow.registerEvent('performed_next_infinite_scroll', () => {
75
+ this.properties = this.getProperties();
76
+ this.setMarkers();
77
+ })
78
+ }
79
+
62
80
  init() {
63
- store.dispatch(setSearchField({ page: null }));
81
+ if (!this.updateMapOnPropertiesLoadMore) store.dispatch(setSearchField({ page: null }));
82
+
64
83
  //@setElement( $(Homeflow.get('small_map_element')))
65
84
  if (Homeflow.get('draggable_map_view') != null) {
66
85
  this.element = Homeflow.get('draggable_map_view');
@@ -81,6 +100,9 @@ export default class DraggableMap {
81
100
  }
82
101
  Homeflow.kickEvent('map_view_rendered', this);
83
102
  store.dispatch(setLoading({ propertiesMap: false }));
103
+
104
+ if (this.updateMapOnPropertiesLoadMore) this.initMapUpdateOnPropertiesLoadMore();
105
+
84
106
  if (Homeflow.get('draw_map_search_loader')) {
85
107
  this.drawSearchLoader = document.querySelector(`${Homeflow.get('draw_map_search_loader')}`);
86
108
  }
@@ -476,8 +498,12 @@ export default class DraggableMap {
476
498
 
477
499
 
478
500
  onMapDrag({ markerPlaceId } = { markerPlaceId: null }) {
501
+ // no drag allowed.
479
502
  if (Homeflow.get('disable_draggable_map') || this.drawableMapInitialized) return;
480
503
 
504
+ // drag map but no fetching properties within the new boundaries.
505
+ if(this.dragWithoutUpdate) return null;
506
+
481
507
  let url;
482
508
  const bounds = this.getSearchableBounds();
483
509
  const has_expanded = this.getSearch().expandedPolygon;
@@ -16,7 +16,7 @@ import './marker_cluster.css';
16
16
  import './marker_cluster.default.css';
17
17
 
18
18
  // global callback to run when maps scripts are loaded
19
- window.initLegacyMap = (options) => {
19
+ window.initLegacyMap = (options, updateMap, updateDragg) => {
20
20
  let map;
21
21
  if (Homeflow.get('geonames_map')) {
22
22
  map = new GeonamesMap();
@@ -25,7 +25,10 @@ window.initLegacyMap = (options) => {
25
25
  map = new DrawableMap();
26
26
  map.init();
27
27
  } else {
28
- map = new DraggableMap();
28
+ map = new DraggableMap({
29
+ updateMapOnPropertiesLoadMore: updateMap,
30
+ dragWithoutUpdate: updateDragg,
31
+ });
29
32
  map.init();
30
33
  }
31
34
 
@@ -67,7 +70,9 @@ window.$ = () => ({
67
70
  // )
68
71
  // end
69
72
 
70
- const PropertiesMap = ({ leaflet, gmapKey, googleLayer, legacyMapOptions }) => {
73
+ const PropertiesMap = ({
74
+ leaflet, gmapKey, googleLayer, legacyMapOptions, updateMapOnPropertiesLoadMore, dragWithoutUpdate
75
+ }) => {
71
76
  const addLegacyMaps = () => {
72
77
  const needsGoogle = !leaflet || googleLayer;
73
78
 
@@ -84,7 +89,7 @@ const PropertiesMap = ({ leaflet, gmapKey, googleLayer, legacyMapOptions }) => {
84
89
  script.setAttribute('id', 'hfjs-legacy-maps');
85
90
  script.setAttribute(
86
91
  'onload',
87
- `window.initLegacyMap(${legacyMapOptions ? JSON.stringify(legacyMapOptions) : ''})`,
92
+ `window.initLegacyMap(${legacyMapOptions ? JSON.stringify(legacyMapOptions) : null}, ${updateMapOnPropertiesLoadMore}, ${dragWithoutUpdate})`,
88
93
  );
89
94
  document.head.appendChild(script);
90
95
  } else {
@@ -108,6 +113,8 @@ PropertiesMap.propTypes = {
108
113
  gmapKey: PropTypes.string,
109
114
  leaflet: PropTypes.bool,
110
115
  googleLayer: PropTypes.bool,
116
+ updateMapOnPropertiesLoadMore: PropTypes.bool,
117
+ dragWithoutUpdate: PropTypes.bool,
111
118
  legacyMapOptions: PropTypes.shape({
112
119
  disableHandlersOnInit: PropTypes.bool,
113
120
  overlayText: PropTypes.string,
@@ -119,6 +126,8 @@ PropertiesMap.defaultProps = {
119
126
  leaflet: false,
120
127
  googleLayer: false,
121
128
  legacyMapOptions: null,
129
+ updateMapOnPropertiesLoadMore: false,
130
+ dragWithoutUpdate: false,
122
131
  };
123
132
 
124
133
  const mapStateToProps = (state) => ({
@@ -0,0 +1,205 @@
1
+ import React, {
2
+ useState, useEffect, useRef, useCallback,
3
+ } from 'react';
4
+ import PropTypes from 'prop-types';
5
+ import debounce from 'lodash.debounce';
6
+ import Input from './input';
7
+
8
+ const DEFAULT_ADDRESS = {
9
+ value: {},
10
+ error: null,
11
+ };
12
+
13
+ const AddressLookupInput = ({
14
+ label,
15
+ placeholder,
16
+ className,
17
+ name,
18
+ required,
19
+ setSelectedAddress,
20
+ }) => {
21
+ const [locationQuery, setLocationQuery] = useState('');
22
+ const [suggestions, setSuggestions] = useState([]);
23
+ const [loading, setLoading] = useState(false);
24
+ const [address, setAddress] = useState(DEFAULT_ADDRESS);
25
+
26
+ const addressLookup = (value) => {
27
+ setLoading(true);
28
+ // If no value or is loading just hide suggestions
29
+ if (!value || loading) setSuggestions([]);
30
+
31
+ fetch(`/address_lookup?autocomplete=${value}`)
32
+ .then(response => response.json())
33
+ .then((json) => {
34
+ if (json.suggestions.length === 0) {
35
+ setAddress({
36
+ value: address.value,
37
+ error: 'Sorry we couldn\'t find that address',
38
+ });
39
+ }
40
+ if (json.suggestions.length !== 0) {
41
+ setAddress({
42
+ value: address.value,
43
+ error: null,
44
+ });
45
+ setSuggestions(json.suggestions);
46
+ }
47
+ setLoading(false);
48
+ });
49
+ };
50
+
51
+ const debouncedAddressLookup = useCallback(debounce(addressLookup, 500), []);
52
+
53
+ const handleQueryChange = (e) => {
54
+ const { value } = e.target;
55
+
56
+ setLocationQuery(value);
57
+ debouncedAddressLookup(value);
58
+ };
59
+
60
+ const handleAddressSelect = (id, address) => {
61
+ fetch(`/address_lookup?id=${id}`)
62
+ .then(response => response.json())
63
+ .then((json) => {
64
+ setLocationQuery(address);
65
+ setAddress({
66
+ value: json,
67
+ error: null,
68
+ });
69
+ if (setSelectedAddress) {
70
+ setSelectedAddress(json);
71
+ }
72
+ setLoading(false);
73
+ });
74
+ };
75
+
76
+ // This handles triggering the search
77
+ const clearSearch = () => {
78
+ setLocationQuery('');
79
+ setSuggestions([]);
80
+ setAddress({
81
+ value: {},
82
+ error: null,
83
+ });
84
+ };
85
+
86
+ // If there is an error remove suggestions
87
+ useEffect(() => {
88
+ if (address.error) setSuggestions([]);
89
+ }, [address.error]);
90
+
91
+
92
+ /**
93
+ * If loading setSuggestions to an empty array,
94
+ * this stops the suggestions updating sporadically
95
+ * as the user is entering a new query
96
+ */
97
+ useEffect(() => {
98
+ if (loading) setSuggestions([]);
99
+ }, [loading]);
100
+
101
+ /**
102
+ * This will clear the input and close our dropdown if the user click
103
+ * outside of the input, this was in its own hook but was causing a
104
+ * memory leak
105
+ */
106
+ const inputRef = useRef(null);
107
+ const [clickedOutSide, setClickedOutside] = useState(null);
108
+ const closeDropdown = () => {
109
+ if (Object.keys(address.value).length === 0 && !loading) {
110
+ setLocationQuery('');
111
+ setSuggestions([]);
112
+ }
113
+ };
114
+ useEffect(() => {
115
+ // If clicked on outside of element
116
+ const handleClickOutside = (event) => {
117
+ /**
118
+ * Check if the user has clicked outside of the component,
119
+ * close the dropdown. However,we only ever need to check
120
+ * this once so setClickedOutside to true after
121
+ */
122
+ if (inputRef.current && !inputRef.current.contains(event.target) && !clickedOutSide) {
123
+ closeDropdown();
124
+ setClickedOutside(true);
125
+ }
126
+
127
+ // If the user clicks inside the component just set clickedOutSide to false
128
+ if (inputRef.current && inputRef.current.contains(event.target) && clickedOutSide) {
129
+ setClickedOutside(false);
130
+ }
131
+ };
132
+ // Bind the event listener
133
+ document.addEventListener('click', handleClickOutside);
134
+ return () => {
135
+ // Unbind the event listener on clean up
136
+ document.removeEventListener('click', handleClickOutside);
137
+ };
138
+ }, [inputRef, clickedOutSide]);
139
+
140
+ return (
141
+ <div className="address-lookup-input" ref={inputRef}>
142
+ <Input
143
+ id="address-lookup-input"
144
+ name={name}
145
+ data-key="address_lookup"
146
+ label={label}
147
+ placeholder={loading ? 'Loading...' : placeholder}
148
+ value={locationQuery}
149
+ onChange={handleQueryChange}
150
+ onClear={clearSearch}
151
+ address={address}
152
+ loading={loading}
153
+ fullWidth
154
+ onDark={false}
155
+ required={required}
156
+ className={className}
157
+ // autoComplete false won't in chrome but if we set it to some random value it will work
158
+ autoComplete="nope"
159
+ />
160
+ {/* Render the search results */}
161
+ {suggestions.length > 0 && (
162
+ <ul className="address-lookup-input__results">
163
+ {suggestions.map(({ address, id }) => (
164
+ <li
165
+ className="address-lookup-input__results-item"
166
+ key={id}
167
+ >
168
+ <button
169
+ type="button"
170
+ className="address-lookup-input__results-btn btn-unstyled"
171
+ onClick={() => {
172
+ handleAddressSelect(id, address);
173
+ setLoading(true);
174
+ setSuggestions([]);
175
+ }}
176
+ >
177
+ {address}
178
+ </button>
179
+ </li>
180
+ ))}
181
+ </ul>
182
+ )}
183
+ </div>
184
+ );
185
+ };
186
+
187
+ AddressLookupInput.propTypes = {
188
+ label: PropTypes.string,
189
+ placeholder: PropTypes.string,
190
+ name: PropTypes.string,
191
+ required: PropTypes.bool,
192
+ className: PropTypes.string,
193
+ setSelectedAddress: PropTypes.func,
194
+ };
195
+
196
+ AddressLookupInput.defaultProps = {
197
+ label: '',
198
+ placeholder: 'Enter postcode or address',
199
+ name: 'formatted_address',
200
+ required: false,
201
+ className: '',
202
+ setSelectedAddress: null,
203
+ };
204
+
205
+ export default AddressLookupInput;
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import SearchIcon from '../../shared/search-icon.component';
4
+ import CloseIcon from '../../shared/close-icon.component';
5
+ import LoadingIcon from '../../shared/loading-icon.component';
6
+
7
+ const ClearButton = ({ address, loading, onClear }) => (
8
+ <button
9
+ type="button"
10
+ className="address-lookup-input__btn btn-unstyled"
11
+ onClick={onClear}
12
+ // Disable the button if there is no address
13
+ disabled={Object.keys(address.value).length === 0}
14
+ >
15
+ {Object.keys(address.value).length === 0 && !loading && (
16
+ <SearchIcon />
17
+ )}
18
+ {Object.keys(address.value).length !== 0 && !loading && (
19
+ <CloseIcon />
20
+ )}
21
+ {loading && (
22
+ <LoadingIcon />
23
+ )}
24
+ </button>
25
+ );
26
+
27
+ ClearButton.propTypes = {
28
+ address: PropTypes.object.isRequired,
29
+ loading: PropTypes.bool.isRequired,
30
+ onClear: PropTypes.func.isRequired,
31
+ };
32
+
33
+ export default ClearButton;
@@ -0,0 +1,88 @@
1
+ /* eslint-disable jsx-a11y/label-has-for */
2
+ import React from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import ClearButton from './clear-button.component';
5
+
6
+ const Input = ({
7
+ type,
8
+ id,
9
+ name,
10
+ label,
11
+ value,
12
+ onChange,
13
+ onClear,
14
+ placeholder,
15
+ address,
16
+ fullWidth,
17
+ onDark,
18
+ iconLeft,
19
+ iconRight,
20
+ disabled,
21
+ className,
22
+ required,
23
+ loading,
24
+ ...other
25
+ }) => (
26
+ <div className={`input ${address?.error ? 'input--error' : ''} ${fullWidth ? 'input--full-width' : ''} ${onDark ? 'input--on-dark' : ''}`}>
27
+ {label && (
28
+ <label className="input__label" htmlFor={id}>{label}</label>
29
+ )}
30
+ <div className="input__wrapper">
31
+ {iconLeft}
32
+ <input
33
+ type={type}
34
+ className={`input__input ${className || ''}`}
35
+ placeholder={placeholder}
36
+ id={id}
37
+ name={name}
38
+ value={value}
39
+ onChange={onChange}
40
+ disabled={disabled}
41
+ required={required}
42
+ {...other}
43
+ />
44
+ <ClearButton address={address} loading={loading} onClear={onClear} />
45
+ </div>
46
+ {address?.error && typeof (address.error) === 'string' && (
47
+ <span className="input__error">{address.error}</span>
48
+ )}
49
+ </div>
50
+ );
51
+
52
+ Input.propTypes = {
53
+ type: PropTypes.string,
54
+ id: PropTypes.string.isRequired,
55
+ name: PropTypes.string.isRequired,
56
+ label: PropTypes.string,
57
+ value: PropTypes.any.isRequired,
58
+ onChange: PropTypes.func.isRequired,
59
+ onClear: PropTypes.func.isRequired,
60
+ placeholder: PropTypes.string,
61
+ address: PropTypes.object.isRequired,
62
+ fullWidth: PropTypes.bool,
63
+ onDark: PropTypes.bool,
64
+ iconLeft: PropTypes.node,
65
+ iconRight: PropTypes.node,
66
+ loading: PropTypes.bool.isRequired,
67
+ disabled: PropTypes.oneOfType([
68
+ PropTypes.string,
69
+ PropTypes.bool,
70
+ ]),
71
+ className: PropTypes.string,
72
+ required: PropTypes.bool,
73
+ };
74
+
75
+ Input.defaultProps = {
76
+ type: 'text',
77
+ label: '',
78
+ placeholder: '',
79
+ fullWidth: false,
80
+ onDark: true,
81
+ iconLeft: null,
82
+ iconRight: null,
83
+ disabled: false,
84
+ className: '',
85
+ required: false,
86
+ };
87
+
88
+ export default Input;
package/search/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import SearchForm from './search-form/search-form.component';
2
2
  import LocationInput from './location-input/location-input.component';
3
+ import AddressLookupInput from './address-lookup-input/address-lookup-input.component';
3
4
  import BedroomsSelect from './bedrooms-select/bedrooms-select.component';
4
5
  import ChannelRadioButton from './channel-radio-button/channel-radio-button.component';
5
6
  import PriceSelect from './price-select/price-select.component';
@@ -17,6 +18,7 @@ import generateSearchDescription from './saved-search/generate-description';
17
18
  export {
18
19
  SearchForm,
19
20
  LocationInput,
21
+ AddressLookupInput,
20
22
  BedroomsSelect,
21
23
  ChannelRadioButton,
22
24
  PriceSelect,
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+
3
+ const CloseIcon = () => (
4
+ <svg role="img" xmlns="http://www.w3.org/2000/svg" width="20.52" height="20.52" viewBox="0 0 20.52 20.52">
5
+ <title>Close Icon</title>
6
+ <g transform="translate(-336.011 -145.97)">
7
+ <line x2="19.459" y2="19.459" transform="translate(336.541 146.5)" fill="none" stroke="inherit" strokeWidth="1.5" />
8
+ <line x1="19.459" y2="19.459" transform="translate(336.541 146.5)" fill="none" stroke="inherit" strokeWidth="1.5" />
9
+ </g>
10
+ </svg>
11
+ );
12
+
13
+ export default CloseIcon;
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+
3
+ const LoadingIcon = () => (
4
+ <svg role="img" className="loading-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
5
+ <title>Loading Icon</title>
6
+ <path d="M23 4V10H17" stroke="inherit" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
7
+ <path d="M20.49 15C19.84 16.8399 18.6096 18.4187 16.9842 19.4985C15.3588 20.5783 13.4265 21.1006 11.4784 20.9866C9.53038 20.8726 7.67216 20.1286 6.18376 18.8667C4.69536 17.6047 3.65743 15.8932 3.22637 13.9901C2.79531 12.0869 2.99448 10.0952 3.79386 8.31508C4.59325 6.53496 5.94954 5.06288 7.65836 4.12065C9.36717 3.17843 11.3359 2.81711 13.268 3.09116C15.2 3.3652 16.9906 4.25975 18.37 5.64001L23 10" stroke="inherit" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
8
+ </svg>
9
+ );
10
+
11
+ export default LoadingIcon;
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const SearchIcon = ({ inherit }) => (
5
+ <svg role="img" xmlns="http://www.w3.org/2000/svg" width="15.97" height="14.197" viewBox="0 0 15.97 14.197">
6
+ <title>Search Icon</title>
7
+ <g transform="translate(-1033.533 -21.389)">
8
+ <g transform="translate(1033.533 21.389)" fill="none" stroke={`${inherit ? 'inherit' : '#fff'}`} strokeWidth="1.5">
9
+ <ellipse cx="6.805" cy="6.806" rx="6.805" ry="6.806" stroke="none" />
10
+ <ellipse cx="6.805" cy="6.806" rx="6.055" ry="6.056" fill="none" />
11
+ </g>
12
+ <line x1="3.781" y1="3.025" transform="translate(1045.254 31.976)" fill="none" stroke={`${inherit ? 'inherit' : '#fff'}`} strokeWidth="1.5" />
13
+ </g>
14
+ </svg>
15
+ );
16
+
17
+ SearchIcon.propTypes = {
18
+ inherit: PropTypes.bool,
19
+ };
20
+
21
+ SearchIcon.defaultProps = {
22
+ inherit: false,
23
+ };
24
+
25
+ export default SearchIcon;