@teipublisher/pb-components 1.30.4 → 1.32.2

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.
@@ -1,5 +1,5 @@
1
1
  import { LitElement, html, css } from 'lit-element';
2
- import * as L from 'leaflet/dist/leaflet-src.esm.js';
2
+ import "@lrnwebcomponents/es-global-bridge";
3
3
  import { pbMixin } from './pb-mixin.js';
4
4
  import { resolveURL } from './utils.js';
5
5
  import './pb-map-layer.js';
@@ -9,9 +9,22 @@ import './pb-map-layer.js';
9
9
  *
10
10
  * @slot - may contain a series of `pb-map-layer` configurations
11
11
  * @fires pb-leaflet-marker-click - Fires event to be processed by the map upon click
12
- * @fires pb-update-map - When received, redraws the map to fit markers passed in with the event
13
- * @fires pb-update - When received, redraws the map to show markers for all pb-geolocation elements
14
- * @fires pb-geolocation - When received, focuses the map on the geocoordinates passed in with the event
12
+ * @fires pb-update-map - When received, redraws the map to fit markers passed in with the event.
13
+ * Event details should include an array of locations, see `pb-geolocation` event below.
14
+ * @fires pb-update - When received, redraws the map to show markers for all pb-geolocation elements found in the content of the pb-view
15
+ * @fires pb-geolocation - When received, focuses the map on the geocoordinates passed in with the event.
16
+ * The event details should include an object:
17
+ * ```
18
+ * {
19
+ * coordinates: {
20
+ * latitude: Number,
21
+ * longitude: Number
22
+ * },
23
+ * label: string - the label to show on mouseover,
24
+ * zoom: Number - fixed zoom level to zoom to,
25
+ * fitBounds: Boolean - if true, recompute current zoom level to show all markers
26
+ * }
27
+ * ```
15
28
  */
16
29
  export class PbLeafletMap extends pbMixin(LitElement) {
17
30
  static get properties() {
@@ -29,6 +42,30 @@ export class PbLeafletMap extends pbMixin(LitElement) {
29
42
  crs: {
30
43
  type: String
31
44
  },
45
+ /**
46
+ * If set, the map will automatically zoom so it can fit all the markers
47
+ */
48
+ fitMarkers: {
49
+ type: Boolean,
50
+ attribute: 'fit-markers'
51
+ },
52
+ /**
53
+ * If set, combine markers into clusters if they are located too close together
54
+ * to display as single markers
55
+ */
56
+ cluster: {
57
+ type: Boolean
58
+ },
59
+ /**
60
+ * Limits up to which zoom level markers are arranged into clusters.
61
+ * Using a higher zoom level here will result in more markers to be shown.
62
+ *
63
+ * Requires `cluster` option to be enabled.
64
+ */
65
+ disableClusteringAt: {
66
+ type: Number,
67
+ attribute: 'disable-clustering-at'
68
+ },
32
69
  /**
33
70
  * If enabled, the map will not automatically scroll to the coordinates received via `pb-geolocation`
34
71
  */
@@ -73,6 +110,9 @@ export class PbLeafletMap extends pbMixin(LitElement) {
73
110
  this.toggle = false;
74
111
  this.noScroll = false;
75
112
  this.disabled = true;
113
+ this.cluster = false;
114
+ this.fitMarkers = false;
115
+ this.disableClusteringAt = null;
76
116
  }
77
117
 
78
118
  connectedCallback() {
@@ -85,7 +125,8 @@ export class PbLeafletMap extends pbMixin(LitElement) {
85
125
  * @param {{ detail: any[]; }} ev
86
126
  */
87
127
  this.subscribeTo('pb-update-map', (ev) => {
88
- const bounds = L.latLngBounds();
128
+ this._markerLayer.clearLayers();
129
+
89
130
  /**
90
131
  * @param {{ latitude: any; longitude: any; label: any; }} loc
91
132
  */
@@ -101,14 +142,9 @@ export class PbLeafletMap extends pbMixin(LitElement) {
101
142
  this.emitTo('pb-leaflet-marker-click', loc);
102
143
  });
103
144
  marker.bindTooltip(loc.label);
104
- marker.addTo(this._map);
105
- bounds.extend([loc.latitude, loc.longitude]);
145
+ this._markerLayer.addLayer(marker);
106
146
  });
107
- if (ev.detail.length > 1) {
108
- this._map.fitBounds(bounds);
109
- } else {
110
- this._map.setZoom(this.zoom);
111
- }
147
+ this._fitBounds();
112
148
  });
113
149
 
114
150
  /**
@@ -117,20 +153,14 @@ export class PbLeafletMap extends pbMixin(LitElement) {
117
153
  * @param {{ detail: { root: { querySelectorAll: (arg0: string) => any[]; }; }; }} ev
118
154
  */
119
155
  this.subscribeTo('pb-update', (ev) => {
120
- this._map.eachLayer((layer) => {
121
- if (layer instanceof L.Marker) {
122
- layer.remove();
123
- }
124
- });
125
- const bounds = L.latLngBounds();
156
+ this._markerLayer.clearLayers();
126
157
  const locations = ev.detail.root.querySelectorAll('pb-geolocation');
127
158
  /**
128
159
  * @param {{ latitude: any; longitude: any; }} loc
129
160
  */
130
161
  locations.forEach((loc) => {
131
162
  const coords = L.latLng(loc.latitude, loc.longitude);
132
- bounds.extend(coords);
133
- const marker = L.marker(coords).addTo(this._map);
163
+ const marker = L.marker(coords).addTo(this._markerLayer);
134
164
  if (loc.label) {
135
165
  marker.bindTooltip(loc.label);
136
166
  }
@@ -141,12 +171,7 @@ export class PbLeafletMap extends pbMixin(LitElement) {
141
171
  this.emitTo('pb-leaflet-marker-click', loc);
142
172
  });
143
173
  });
144
- // this._map.invalidateSize();
145
- if (locations.length >= 1) {
146
- this._map.fitBounds(bounds);
147
- } else {
148
- this._map.fitWorld();
149
- }
174
+ this._fitBounds();
150
175
  });
151
176
 
152
177
  /**
@@ -169,7 +194,12 @@ export class PbLeafletMap extends pbMixin(LitElement) {
169
194
  if (ev.detail.popup) {
170
195
  marker.bindPopup(ev.detail.popup);
171
196
  }
172
- marker.addTo(this._map);
197
+ marker.addTo(this._markerLayer);
198
+
199
+ if (ev.detail.fitBounds) {
200
+ this._fitBounds();
201
+ }
202
+
173
203
  console.log('<pb-leaflet-map> added marker');
174
204
  } else {
175
205
  console.log('<pb-leaflet-map> Marker already added to map');
@@ -177,7 +207,7 @@ export class PbLeafletMap extends pbMixin(LitElement) {
177
207
  if (this.toggle) {
178
208
  this.disabled = false;
179
209
  }
180
- this._locationChanged();
210
+ this._locationChanged(this.latitude, this.longitude, ev.detail.zoom);
181
211
  }
182
212
  });
183
213
  }
@@ -186,13 +216,23 @@ export class PbLeafletMap extends pbMixin(LitElement) {
186
216
  if (!this.toggle) {
187
217
  this.disabled = false;
188
218
  }
189
- this._initMap();
219
+ window.ESGlobalBridge.requestAvailability();
220
+ const leafletPath = resolveURL('../lib/leaflet-src.js');
221
+ const pluginPath = resolveURL('../lib/leaflet.markercluster-src.js');
222
+ window.ESGlobalBridge.instance.load("leaflet", leafletPath)
223
+ .then(() => window.ESGlobalBridge.instance.load("plugin", pluginPath));
224
+ window.addEventListener(
225
+ "es-bridge-plugin-loaded",
226
+ this._initMap.bind(this),
227
+ { once: true }
228
+ );
190
229
  }
191
230
 
192
231
  render() {
193
232
  const cssPath = resolveURL(this.cssPath);
194
233
  return html`
195
234
  <link rel="Stylesheet" href="${cssPath}/leaflet.css">
235
+ <link rel="Stylesheet" href="${cssPath}/MarkerCluster.Default.css">
196
236
  <div id="map" style="height: 100%; width: 100%"></div>
197
237
  `;
198
238
  }
@@ -234,6 +274,18 @@ export class PbLeafletMap extends pbMixin(LitElement) {
234
274
  crs
235
275
  });
236
276
  this._configureLayers();
277
+
278
+ if (this.cluster) {
279
+ const options = {};
280
+ if (this.disableClusteringAt) {
281
+ options.disableClusteringAtZoom = this.disableClusteringAt;
282
+ }
283
+ this._markerLayer = L.markerClusterGroup(options);
284
+ } else {
285
+ this._markerLayer = L.layerGroup();
286
+ }
287
+ this._markerLayer.addTo(this._map);
288
+
237
289
  this.signalReady();
238
290
 
239
291
  L.control.scale().addTo(this._map);
@@ -308,27 +360,50 @@ export class PbLeafletMap extends pbMixin(LitElement) {
308
360
  }
309
361
  }
310
362
 
311
- _locationChanged() {
363
+ _fitBounds() {
364
+ if (!this.fitMarkers) {
365
+ return;
366
+ }
367
+ const bounds = L.latLngBounds();
368
+ let len = 0;
369
+ this._markerLayer.eachLayer((layer) => {
370
+ bounds.extend(layer.getLatLng());
371
+ len += 1;
372
+ });
373
+ if (len === 0) {
374
+ this._map.fitWorld();
375
+ } else if (len === 1) {
376
+ this._map.fitBounds(bounds, {maxZoom: this.zoom});
377
+ } else {
378
+ this._map.fitBounds(bounds);
379
+ }
380
+ }
381
+
382
+ _locationChanged(lat, long, zoom) {
312
383
  if (this._map) {
313
- const coords = L.latLng([this.latitude, this.longitude]);
314
- this._map.eachLayer((layer) => {
315
- if (layer instanceof L.Marker) {
316
- if (layer.getLatLng().equals(coords)) {
384
+ const coords = L.latLng([lat, long]);
385
+ this._markerLayer.eachLayer((layer) => {
386
+ if (layer.getLatLng().equals(coords)) {
387
+ if (zoom && !this.noScroll) {
317
388
  layer.openTooltip();
389
+ this._map.setView(coords, zoom);
390
+ } else if (this.cluster) {
391
+ this._markerLayer.zoomToShowLayer(layer, () =>
392
+ layer.openTooltip()
393
+ );
318
394
  } else {
319
- layer.closeTooltip();
395
+ layer.openTooltip();
396
+ this._map.setView(coords, this.zoom);
320
397
  }
321
398
  }
322
399
  });
323
- if (!this.noScroll)
324
- this._map.setView(coords, this.zoom);
325
400
  }
326
401
  }
327
402
 
328
403
  _hasMarker(lat, long) {
329
404
  const coords = L.latLng([lat, long]);
330
405
  let found = false;
331
- this._map.eachLayer((layer) => {
406
+ this._markerLayer.eachLayer((layer) => {
332
407
  if (layer instanceof L.Marker && layer.getLatLng().equals(coords)) {
333
408
  found = true;
334
409
  }
@@ -0,0 +1,188 @@
1
+ import { LitElement, html, css } from 'lit-element';
2
+ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
3
+ import { pbMixin } from './pb-mixin.js';
4
+
5
+ /**
6
+ * Implements a list which is split into different categories
7
+ * (e.g. letters of the alphabet, countries ...).
8
+ * Only one category is shown at a time unless the server reports
9
+ * no categories (e.g. if the number of items to display goes below
10
+ * a defined threshold).
11
+ *
12
+ * The server-side API endpoint should return a JSON object with two
13
+ * properties:
14
+ *
15
+ * + `categories`: an array of category descriptions: each item should
16
+ * be an object with two properties: `category` - containing the name of the category
17
+ * and `count` - containing a count of items available under this category.
18
+ * + `items`: an array with the items to be shown for the currently selected
19
+ * category. Those may contain HTML markup.
20
+ *
21
+ * @cssprop --pb-categorized-list-columns - the number of columns to display (default: 2)
22
+ * @fires pb-submit - when received, submit a request to the server and refresh
23
+ * @fires pb-start-update - sent before the element sends the request to the server
24
+ * @fires pb-end-update - sent after new content has been received
25
+ */
26
+ export class PbSplitList extends pbMixin(LitElement) {
27
+ static get properties() {
28
+ return {
29
+ /**
30
+ * Server-side API endpoint to retrieve items from
31
+ */
32
+ url: {
33
+ type: String
34
+ },
35
+ /**
36
+ * The initially selected category
37
+ */
38
+ selected: {
39
+ type: String
40
+ },
41
+ /**
42
+ * A CSS selector pointing to one or more `pb-custom-form`
43
+ * instances. The element will collect additional parameters
44
+ * from those forms and includes them in the request to the server
45
+ */
46
+ subforms: {
47
+ type: String
48
+ },
49
+ _categories: {
50
+ type: Array
51
+ },
52
+ ...super.properties
53
+ };
54
+ }
55
+
56
+ constructor() {
57
+ super();
58
+ this._categories = [];
59
+ this._params = {};
60
+ this.selected = null;
61
+ this.subforms = null;
62
+ }
63
+
64
+ connectedCallback() {
65
+ super.connectedCallback();
66
+
67
+ this.selected = this.getParameter('category', this.selected);
68
+
69
+ window.addEventListener('popstate', (ev) => {
70
+ console.log('<pb-split-list> popstate: %o', ev);
71
+ this.selected = ev.state.category;
72
+ this.submit();
73
+ });
74
+
75
+ this.subscribeTo('pb-submit', this.load.bind(this));
76
+ }
77
+
78
+ firstUpdated() {
79
+ super.firstUpdated();
80
+
81
+ PbSplitList.waitOnce('pb-page-ready', () => {
82
+ this.load();
83
+ });
84
+ }
85
+
86
+ submit() {
87
+ this.load();
88
+ }
89
+
90
+ load() {
91
+ const formParams = this._paramsFromSubforms({ category: this.selected });
92
+ this.setParameters(formParams);
93
+ this.pushHistory('pb-split-list', formParams);
94
+
95
+ const params = new URLSearchParams(formParams);
96
+
97
+ const url = `${this.toAbsoluteURL(this.url)}?${params.toString()}`;
98
+ console.log(`<pb-split-list> Fetching from URL: ${url}`);
99
+
100
+ this.emitTo('pb-start-update');
101
+
102
+ fetch(url)
103
+ .then((response) => {
104
+ if (response.ok) {
105
+ return response.json();
106
+ }
107
+ return Promise.reject(response.status);
108
+ })
109
+ .then((json) => {
110
+ this._categories = json.categories;
111
+ this.innerHTML = json.items.join('');
112
+ this.emitTo('pb-end-update');
113
+ })
114
+ .catch((error) => {
115
+ console.error(`<pb-split-list> Error caught: ${error}`);
116
+ this.emitTo('pb-end-update');
117
+ });
118
+ }
119
+
120
+ _selectCategory(ev, category) {
121
+ ev.preventDefault();
122
+ this.selected = category;
123
+ this.load();
124
+ }
125
+
126
+ _paramsFromSubforms(params) {
127
+ if (this.subforms) {
128
+ document.querySelectorAll(this.subforms).forEach((form) => {
129
+ if (form.serializeForm) {
130
+ Object.assign(params, form.serializeForm());
131
+ }
132
+ });
133
+ }
134
+ return params;
135
+ }
136
+
137
+ render() {
138
+ return html`
139
+ <header>
140
+ ${
141
+ this._categories.map((cat) =>
142
+ html`
143
+ <a part="${this.selected === cat.category ? 'active-category' : 'category'}" href="#${cat.category}" title="${cat.count}" class="${this.selected === cat.category ? 'active' : ''}"
144
+ @click="${(ev) => this._selectCategory(ev, cat.category)}">
145
+ ${cat.category}
146
+ </a>
147
+ `
148
+ )
149
+ }
150
+ </header>
151
+ <div id="items" part="items"><slot></slot></div>
152
+ `;
153
+ }
154
+
155
+ static get styles() {
156
+ return css`
157
+ :host {
158
+ display: block;
159
+ }
160
+
161
+ header {
162
+ display: flex;
163
+ flex-wrap: wrap;
164
+ column-gap: 10px;
165
+ width: 100%;
166
+ }
167
+
168
+ #items {
169
+ display: grid;
170
+ grid-template-columns: repeat(var(--pb-categorized-list-columns, 2), auto);
171
+ grid-auto-rows: 1fr;
172
+ column-gap: 10px;
173
+ width: 100%;
174
+ }
175
+
176
+ [part=category], #items a {
177
+ text-decoration: none;
178
+ color: var(--pb-link-color);
179
+ }
180
+
181
+ [part=active-category] {
182
+ text-decoration: none;
183
+ color: var(--pb-highlight-color);
184
+ }
185
+ `;
186
+ }
187
+ }
188
+ customElements.define('pb-split-list', PbSplitList);