@braudypedrosa/bp-listings 1.0.7 → 1.1.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.
- package/README.md +17 -103
- package/listings-map.css +1138 -53
- package/listings-map.js +286 -14
- package/listings-map.scss +67 -84
- package/package.json +11 -8
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Airbnb-style listings + map widget built with vanilla JavaScript and CSS.
|
|
4
4
|
|
|
5
|
-
Current version: **1.0
|
|
5
|
+
Current version: **1.1.0**
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
@@ -115,8 +115,8 @@ When you integrate external search through `renderSearchSlot`, the recommended c
|
|
|
115
115
|
Important:
|
|
116
116
|
|
|
117
117
|
- this is a docs/demo convention only
|
|
118
|
-
- `bp-listings` does not parse or enforce `searchData`
|
|
119
|
-
- matching remains consumer-owned
|
|
118
|
+
- `bp-listings` does not parse or enforce `searchData` automatically
|
|
119
|
+
- matching remains consumer-owned, but helper exports are available for the documented convention
|
|
120
120
|
|
|
121
121
|
## Config Options
|
|
122
122
|
|
|
@@ -158,7 +158,6 @@ This pattern requires a bundler or dev server that can resolve npm packages and
|
|
|
158
158
|
```js
|
|
159
159
|
import '@braudypedrosa/bp-listings';
|
|
160
160
|
import '@braudypedrosa/bp-listings/styles';
|
|
161
|
-
import '@braudypedrosa/bp-calendar/styles';
|
|
162
161
|
import '@braudypedrosa/bp-search-widget/styles';
|
|
163
162
|
import { BPSearchWidget } from '@braudypedrosa/bp-search-widget';
|
|
164
163
|
|
|
@@ -241,103 +240,10 @@ const filterDefinitions = [
|
|
|
241
240
|
},
|
|
242
241
|
];
|
|
243
242
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const normalizeString = (value) => {
|
|
249
|
-
if (value === null || value === undefined) return '';
|
|
250
|
-
return String(value).trim().toLowerCase();
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const toArray = (value) => {
|
|
254
|
-
if (Array.isArray(value)) return value;
|
|
255
|
-
if (value === null || value === undefined) return [];
|
|
256
|
-
return [value];
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const normalizeStringArray = (value) =>
|
|
260
|
-
toArray(value).map((entry) => normalizeString(entry)).filter(Boolean);
|
|
261
|
-
|
|
262
|
-
const matchesSubstring = (haystackValue, needleValue) => {
|
|
263
|
-
const needle = normalizeString(needleValue);
|
|
264
|
-
if (!needle) return true;
|
|
265
|
-
return normalizeStringArray(haystackValue).some((value) => value.includes(needle));
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const matchesExact = (haystackValue, needleValue) => {
|
|
269
|
-
const needle = normalizeString(needleValue);
|
|
270
|
-
if (!needle) return true;
|
|
271
|
-
return normalizeStringArray(haystackValue).some((value) => value === needle);
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
const matchesAllChoices = (haystackValue, needleValues) => {
|
|
275
|
-
const selectedValues = normalizeStringArray(needleValues);
|
|
276
|
-
if (selectedValues.length === 0) return true;
|
|
277
|
-
const availableValues = new Set(normalizeStringArray(haystackValue));
|
|
278
|
-
return selectedValues.every((value) => availableValues.has(value));
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const matchesCounter = (haystackValue, needleValue) => {
|
|
282
|
-
const numericNeedle = Number(needleValue);
|
|
283
|
-
const numericHaystack = Number(haystackValue);
|
|
284
|
-
if (!Number.isFinite(numericNeedle)) return true;
|
|
285
|
-
if (!Number.isFinite(numericHaystack)) return false;
|
|
286
|
-
return numericHaystack >= numericNeedle;
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const isValidDateString = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
290
|
-
|
|
291
|
-
const matchesAvailability = (availability, checkIn, checkOut) => {
|
|
292
|
-
if (!Array.isArray(availability) || !isValidDateString(checkIn) || !isValidDateString(checkOut)) {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return availability.some((windowRange) => (
|
|
297
|
-
windowRange &&
|
|
298
|
-
isValidDateString(windowRange.start) &&
|
|
299
|
-
isValidDateString(windowRange.end) &&
|
|
300
|
-
windowRange.start <= checkIn &&
|
|
301
|
-
windowRange.end >= checkOut
|
|
302
|
-
));
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
const matchValues = (type, listingValue, submittedValue) => {
|
|
306
|
-
if (type === 'checkbox') return matchesAllChoices(listingValue, submittedValue);
|
|
307
|
-
if (type === 'select' || type === 'radio') return matchesExact(listingValue, submittedValue);
|
|
308
|
-
if (type === 'counter') return matchesCounter(listingValue, submittedValue);
|
|
309
|
-
return matchesSubstring(listingValue, submittedValue);
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
const matchListing = (listing, payload) => {
|
|
313
|
-
const searchData = listing.searchData || {};
|
|
314
|
-
const fieldValues = searchData.fields || {};
|
|
315
|
-
const filterValues = searchData.filters || {};
|
|
316
|
-
|
|
317
|
-
if (!matchesSubstring(searchData.location, payload.location)) {
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (payload.checkIn && payload.checkOut) {
|
|
322
|
-
if (!matchesAvailability(searchData.availability, payload.checkIn, payload.checkOut)) {
|
|
323
|
-
return false;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
for (const [key, value] of Object.entries(payload.customFields || {})) {
|
|
328
|
-
if (!matchValues(fieldTypeMap.get(key), fieldValues[key], value)) {
|
|
329
|
-
return false;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
for (const [key, value] of Object.entries(payload.filters || {})) {
|
|
334
|
-
if (!matchValues(fieldTypeMap.get(key), filterValues[key], value)) {
|
|
335
|
-
return false;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return true;
|
|
340
|
-
};
|
|
243
|
+
const matchListing = ListingsMap.createSearchDataMatcher({
|
|
244
|
+
fields: fieldDefinitions,
|
|
245
|
+
filters: filterDefinitions,
|
|
246
|
+
});
|
|
341
247
|
|
|
342
248
|
const listingsWidget = ListingsMap.init({
|
|
343
249
|
container: '#widget',
|
|
@@ -352,7 +258,10 @@ const listingsWidget = ListingsMap.init({
|
|
|
352
258
|
datepickerPlacement: 'auto',
|
|
353
259
|
},
|
|
354
260
|
onSearch: (payload) => {
|
|
355
|
-
const filteredListings =
|
|
261
|
+
const filteredListings = ListingsMap.filterListingsBySearchData(allListings, payload, {
|
|
262
|
+
fields: fieldDefinitions,
|
|
263
|
+
filters: filterDefinitions,
|
|
264
|
+
});
|
|
356
265
|
widget.setListings(filteredListings);
|
|
357
266
|
},
|
|
358
267
|
});
|
|
@@ -378,6 +287,11 @@ Returned widget instance exposes:
|
|
|
378
287
|
- `goToPage(pageNumber)`
|
|
379
288
|
- `destroy()`
|
|
380
289
|
|
|
290
|
+
Helper exports on `window.ListingsMap` or the package module:
|
|
291
|
+
|
|
292
|
+
- `createSearchDataMatcher({ fields, filters })`
|
|
293
|
+
- `filterListingsBySearchData(listings, payload, { fields, filters })`
|
|
294
|
+
|
|
381
295
|
## Styling and Theming
|
|
382
296
|
|
|
383
297
|
All classes are scoped with `.lm-*`.
|
|
@@ -407,7 +321,7 @@ npm run dev
|
|
|
407
321
|
```
|
|
408
322
|
|
|
409
323
|
The local demo uses Vite only for development so it can resolve `@braudypedrosa/bp-search-widget`,
|
|
410
|
-
`@braudypedrosa/bp-
|
|
324
|
+
`@braudypedrosa/bp-ui-components`, and their styles. The published `bp-listings` package format is unchanged.
|
|
411
325
|
|
|
412
326
|
## Notes
|
|
413
327
|
|