@teipublisher/pb-components 1.30.3 → 1.32.1

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';
@@ -12,7 +12,6 @@ import './pb-map-layer.js';
12
12
  * @fires pb-update-map - When received, redraws the map to fit markers passed in with the event
13
13
  * @fires pb-update - When received, redraws the map to show markers for all pb-geolocation elements
14
14
  * @fires pb-geolocation - When received, focuses the map on the geocoordinates passed in with the event
15
-
16
15
  */
17
16
  export class PbLeafletMap extends pbMixin(LitElement) {
18
17
  static get properties() {
@@ -30,6 +29,20 @@ export class PbLeafletMap extends pbMixin(LitElement) {
30
29
  crs: {
31
30
  type: String
32
31
  },
32
+ /**
33
+ * If set, the map will automatically zoom so it can fit all the markers
34
+ */
35
+ fitMarkers: {
36
+ type: Boolean,
37
+ attribute: 'fit-markers'
38
+ },
39
+ /**
40
+ * If set, combine markers into clusters if they are located too close together
41
+ * to display as single markers
42
+ */
43
+ cluster: {
44
+ type: Boolean
45
+ },
33
46
  /**
34
47
  * If enabled, the map will not automatically scroll to the coordinates received via `pb-geolocation`
35
48
  */
@@ -74,6 +87,8 @@ export class PbLeafletMap extends pbMixin(LitElement) {
74
87
  this.toggle = false;
75
88
  this.noScroll = false;
76
89
  this.disabled = true;
90
+ this.cluster = false;
91
+ this.fitMarkers = false;
77
92
  }
78
93
 
79
94
  connectedCallback() {
@@ -86,7 +101,8 @@ export class PbLeafletMap extends pbMixin(LitElement) {
86
101
  * @param {{ detail: any[]; }} ev
87
102
  */
88
103
  this.subscribeTo('pb-update-map', (ev) => {
89
- const bounds = L.latLngBounds();
104
+ this._markerLayer.clearLayers();
105
+
90
106
  /**
91
107
  * @param {{ latitude: any; longitude: any; label: any; }} loc
92
108
  */
@@ -102,14 +118,9 @@ export class PbLeafletMap extends pbMixin(LitElement) {
102
118
  this.emitTo('pb-leaflet-marker-click', loc);
103
119
  });
104
120
  marker.bindTooltip(loc.label);
105
- marker.addTo(this._map);
106
- bounds.extend([loc.latitude, loc.longitude]);
121
+ this._markerLayer.addLayer(marker);
107
122
  });
108
- if (ev.detail.length > 1) {
109
- this._map.fitBounds(bounds);
110
- } else {
111
- this._map.setZoom(this.zoom);
112
- }
123
+ this._fitBounds();
113
124
  });
114
125
 
115
126
  /**
@@ -118,20 +129,14 @@ export class PbLeafletMap extends pbMixin(LitElement) {
118
129
  * @param {{ detail: { root: { querySelectorAll: (arg0: string) => any[]; }; }; }} ev
119
130
  */
120
131
  this.subscribeTo('pb-update', (ev) => {
121
- this._map.eachLayer((layer) => {
122
- if (layer instanceof L.Marker) {
123
- layer.remove();
124
- }
125
- });
126
- const bounds = L.latLngBounds();
132
+ this._markerLayer.clearLayers();
127
133
  const locations = ev.detail.root.querySelectorAll('pb-geolocation');
128
134
  /**
129
135
  * @param {{ latitude: any; longitude: any; }} loc
130
136
  */
131
137
  locations.forEach((loc) => {
132
138
  const coords = L.latLng(loc.latitude, loc.longitude);
133
- bounds.extend(coords);
134
- const marker = L.marker(coords).addTo(this._map);
139
+ const marker = L.marker(coords).addTo(this._markerLayer);
135
140
  if (loc.label) {
136
141
  marker.bindTooltip(loc.label);
137
142
  }
@@ -142,12 +147,7 @@ export class PbLeafletMap extends pbMixin(LitElement) {
142
147
  this.emitTo('pb-leaflet-marker-click', loc);
143
148
  });
144
149
  });
145
- // this._map.invalidateSize();
146
- if (locations.length > 1) {
147
- this._map.fitBounds(bounds);
148
- } else {
149
- this._map.fitWorld();
150
- }
150
+ this._fitBounds();
151
151
  });
152
152
 
153
153
  /**
@@ -159,6 +159,9 @@ export class PbLeafletMap extends pbMixin(LitElement) {
159
159
  if (ev.detail.coordinates) {
160
160
  this.latitude = ev.detail.coordinates.latitude;
161
161
  this.longitude = ev.detail.coordinates.longitude;
162
+ if (ev.detail.zoom) {
163
+ this.zoom = ev.detail.zoom;
164
+ }
162
165
  if (!this._hasMarker(this.latitude, this.longitude)) {
163
166
  const marker = L.marker([this.latitude, this.longitude]);
164
167
  marker.addEventListener('click', () => {
@@ -170,7 +173,12 @@ export class PbLeafletMap extends pbMixin(LitElement) {
170
173
  if (ev.detail.popup) {
171
174
  marker.bindPopup(ev.detail.popup);
172
175
  }
173
- marker.addTo(this._map);
176
+ marker.addTo(this._markerLayer);
177
+
178
+ if (ev.detail.fitBounds) {
179
+ this._fitBounds();
180
+ }
181
+
174
182
  console.log('<pb-leaflet-map> added marker');
175
183
  } else {
176
184
  console.log('<pb-leaflet-map> Marker already added to map');
@@ -178,7 +186,7 @@ export class PbLeafletMap extends pbMixin(LitElement) {
178
186
  if (this.toggle) {
179
187
  this.disabled = false;
180
188
  }
181
- this._locationChanged();
189
+ this._locationChanged(this.latitude, this.longitude, this.zoom);
182
190
  }
183
191
  });
184
192
  }
@@ -187,13 +195,23 @@ export class PbLeafletMap extends pbMixin(LitElement) {
187
195
  if (!this.toggle) {
188
196
  this.disabled = false;
189
197
  }
190
- this._initMap();
198
+ window.ESGlobalBridge.requestAvailability();
199
+ const leafletPath = resolveURL('../lib/leaflet-src.js');
200
+ const pluginPath = resolveURL('../lib/leaflet.markercluster-src.js');
201
+ window.ESGlobalBridge.instance.load("leaflet", leafletPath)
202
+ .then(() => window.ESGlobalBridge.instance.load("plugin", pluginPath));
203
+ window.addEventListener(
204
+ "es-bridge-plugin-loaded",
205
+ this._initMap.bind(this),
206
+ { once: true }
207
+ );
191
208
  }
192
209
 
193
210
  render() {
194
211
  const cssPath = resolveURL(this.cssPath);
195
212
  return html`
196
213
  <link rel="Stylesheet" href="${cssPath}/leaflet.css">
214
+ <link rel="Stylesheet" href="${cssPath}/MarkerCluster.Default.css">
197
215
  <div id="map" style="height: 100%; width: 100%"></div>
198
216
  `;
199
217
  }
@@ -235,6 +253,14 @@ export class PbLeafletMap extends pbMixin(LitElement) {
235
253
  crs
236
254
  });
237
255
  this._configureLayers();
256
+
257
+ if (this.cluster) {
258
+ this._markerLayer = L.markerClusterGroup();
259
+ } else {
260
+ this._markerLayer = L.layerGroup();
261
+ }
262
+ this._markerLayer.addTo(this._map);
263
+
238
264
  this.signalReady();
239
265
 
240
266
  L.control.scale().addTo(this._map);
@@ -309,27 +335,42 @@ export class PbLeafletMap extends pbMixin(LitElement) {
309
335
  }
310
336
  }
311
337
 
312
- _locationChanged() {
338
+ _fitBounds() {
339
+ if (!this.fitMarkers) {
340
+ return;
341
+ }
342
+ const bounds = L.latLngBounds();
343
+ let len = 0;
344
+ this._markerLayer.eachLayer((layer) => {
345
+ bounds.extend(layer.getLatLng());
346
+ len += 1;
347
+ });
348
+ if (len === 0) {
349
+ this._map.fitWorld();
350
+ } else if (len === 1) {
351
+ this._map.fitBounds(bounds, {maxZoom: this.zoom});
352
+ } else {
353
+ this._map.fitBounds(bounds);
354
+ }
355
+ }
356
+
357
+ _locationChanged(lat, long, zoom) {
313
358
  if (this._map) {
314
- const coords = L.latLng([this.latitude, this.longitude]);
315
- this._map.eachLayer((layer) => {
316
- if (layer instanceof L.Marker) {
317
- if (layer.getLatLng().equals(coords)) {
318
- layer.openTooltip();
319
- } else {
320
- layer.closeTooltip();
321
- }
359
+ const coords = L.latLng([lat, long]);
360
+ this._markerLayer.eachLayer((layer) => {
361
+ if (layer.getLatLng().equals(coords)) {
362
+ layer.openTooltip();
322
363
  }
323
364
  });
324
365
  if (!this.noScroll)
325
- this._map.setView(coords, this.zoom);
366
+ this._map.setView(coords, zoom);
326
367
  }
327
368
  }
328
369
 
329
370
  _hasMarker(lat, long) {
330
371
  const coords = L.latLng([lat, long]);
331
372
  let found = false;
332
- this._map.eachLayer((layer) => {
373
+ this._markerLayer.eachLayer((layer) => {
333
374
  if (layer instanceof L.Marker && layer.getLatLng().equals(coords)) {
334
375
  found = true;
335
376
  }
@@ -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);