@kiva/kv-components 3.37.0 → 3.38.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/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.38.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.37.0...@kiva/kv-components@3.38.0) (2023-08-29)
7
+
8
+
9
+ ### Features
10
+
11
+ * kvmap ported to kv components library ([#286](https://github.com/kiva/kv-ui-elements/issues/286)) ([373e53f](https://github.com/kiva/kv-ui-elements/commit/373e53fd407d161e79033826a6063fbd5ca5b2ce))
12
+
13
+
14
+
15
+
16
+
6
17
  # [3.37.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.36.0...@kiva/kv-components@3.37.0) (2023-08-18)
7
18
 
8
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kiva/kv-components",
3
- "version": "3.37.0",
3
+ "version": "3.38.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -42,6 +42,7 @@
42
42
  "tailwindcss": "^3.0.18",
43
43
  "vue": "^2.6.14",
44
44
  "vue-loader": "^15.9.6",
45
+ "vue-meta": "^2.4.0",
45
46
  "vue-router": "^3.5.2",
46
47
  "vue-template-compiler": "^2.6.12"
47
48
  },
@@ -75,5 +76,5 @@
75
76
  "optional": true
76
77
  }
77
78
  },
78
- "gitHead": "a617796f8021d568415c8d07b9a9f97e9c33595e"
79
+ "gitHead": "00704fdebcb089c0ad690f58a3d9790c76d69e3f"
79
80
  }
@@ -3,6 +3,7 @@ import addons from '@storybook/addons';
3
3
  import KvThemeProvider from '../KvThemeProvider.vue';
4
4
  import { defaultTheme, darkTheme } from '@kiva/kv-tokens/configs/kivaColors.cjs';
5
5
  import Vue from 'vue';
6
+ import Meta from 'vue-meta';
6
7
  import VueCompositionApi from '@vue/composition-api';
7
8
  import VueRouter from 'vue-router';
8
9
 
@@ -11,6 +12,9 @@ Vue.use(VueCompositionApi);
11
12
 
12
13
  Vue.use(VueRouter);
13
14
 
15
+ // initialize vue-meta
16
+ Vue.use(Meta);
17
+
14
18
  export const parameters = {
15
19
  actions: { argTypesRegex: "^on[A-Z].*" },
16
20
  controls: {
package/vue/KvMap.vue ADDED
@@ -0,0 +1,370 @@
1
+ <template>
2
+ <div
3
+ class="tw-relative tw-block tw-w-full"
4
+ :style="mapDimensions"
5
+ >
6
+ <div
7
+ :id="`kv-map-holder-${mapId}`"
8
+ :ref="refString"
9
+ class="tw-w-full tw-h-full tw-bg-black"
10
+ :style="{ position: 'absolute' }"
11
+ ></div>
12
+ </div>
13
+ </template>
14
+
15
+ <script>
16
+ export default {
17
+ name: 'KvMap',
18
+ metaInfo() {
19
+ return {
20
+ script: [].concat(!this.hasWebGL ? [
21
+ // leaflet - uses raster tiles for additional browser coverage
22
+ {
23
+ vmid: `leafletjs${this.mapId}`,
24
+ src: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
25
+ async: true,
26
+ defer: true,
27
+ },
28
+ ] : []).concat(this.hasWebGL ? [
29
+ // maplibregl - uses vector tiles and webgl for rendering
30
+ {
31
+ vmid: `maplibregljs${this.mapId}`,
32
+ src: 'https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js',
33
+ async: true,
34
+ defer: true,
35
+ },
36
+ ] : []),
37
+ link: [
38
+ ].concat(!this.hasWebGL ? [
39
+ // leaflet - uses raster tiles for additional browser coverage
40
+ {
41
+ vmid: `leafletcss${this.mapId}`,
42
+ rel: 'stylesheet',
43
+ href: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css',
44
+ },
45
+ ] : []).concat(this.hasWebGL ? [
46
+ // maplibregl - uses vector tiles and webgl for rendering
47
+ {
48
+ vmid: `maplibreglcss${this.mapId}`,
49
+ rel: 'stylesheet',
50
+ href: 'https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css',
51
+ },
52
+ ] : []),
53
+ };
54
+ },
55
+ props: {
56
+ /**
57
+ * Aspect Ration for computed map dimensions
58
+ * We'll divide the container width by this to determine the height
59
+ */
60
+ aspectRatio: {
61
+ type: Number,
62
+ default: 1,
63
+ },
64
+ /**
65
+ * Control how quickly the autoZoom occurs
66
+ */
67
+ autoZoomDelay: {
68
+ type: Number,
69
+ default: 1500,
70
+ },
71
+ /**
72
+ * Set the height to override aspect ratio driven and/or default dimensions
73
+ */
74
+ height: {
75
+ type: Number,
76
+ default: null,
77
+ },
78
+ /**
79
+ * Setting this initialZoom will zoom the map from initialZoom to zoom when the map enters the viewport
80
+ */
81
+ initialZoom: {
82
+ type: Number,
83
+ default: null,
84
+ },
85
+ /**
86
+ * Set the center point latitude
87
+ */
88
+ lat: {
89
+ type: Number,
90
+ default: null,
91
+ },
92
+ /**
93
+ * Set the center point longitude
94
+ */
95
+ long: {
96
+ type: Number,
97
+ default: null,
98
+ },
99
+ /**
100
+ * Set this if there are more than one map on the page
101
+ */
102
+ mapId: {
103
+ type: Number,
104
+ default: 0,
105
+ },
106
+ /**
107
+ * Force use of Leaflet
108
+ */
109
+ useLeaflet: {
110
+ type: Boolean,
111
+ default: false,
112
+ },
113
+ /**
114
+ * Set the width to override aspect ratio driven and/or default dimensions
115
+ */
116
+ width: {
117
+ type: Number,
118
+ default: null,
119
+ },
120
+ /**
121
+ * Default zoom level
122
+ */
123
+ zoomLevel: {
124
+ type: Number,
125
+ default: 4,
126
+ },
127
+ },
128
+ data() {
129
+ return {
130
+ hasWebGL: false,
131
+ leafletReady: false,
132
+ mapInstance: null,
133
+ mapLibreReady: false,
134
+ mapLoaded: false,
135
+ zoomActive: false,
136
+ };
137
+ },
138
+ computed: {
139
+ mapDimensions() {
140
+ // Use container to derive height based on aspect ration + width
141
+ const container = this.$el?.getBoundingClientRect();
142
+ const height = container ? `${container.width / this.aspectRatio}px` : '300px';
143
+ const width = container ? `${container.width}px` : '100%';
144
+ // Override values if deliberate height or width are provided
145
+ return {
146
+ height: this.height ? `${this.height}px` : height,
147
+ width: this.width ? `${this.width}px` : width,
148
+ paddingBottom: this.height ? `${this.height}px` : `${100 / this.aspectRatio}%`,
149
+ };
150
+ },
151
+ refString() {
152
+ return `mapholder${this.mapId}`;
153
+ },
154
+ },
155
+ watch: {
156
+ lat(next, prev) {
157
+ if (prev === null && this.long && !this.mapLibreReady && !this.leafletReady) {
158
+ this.initializeMap();
159
+ }
160
+ },
161
+ long(next, prev) {
162
+ if (prev === null && this.lat && !this.mapLibreReady && !this.leafletReady) {
163
+ this.initializeMap();
164
+ }
165
+ },
166
+ },
167
+ mounted() {
168
+ if (!this.mapLibreReady && !this.leafletReady) {
169
+ this.initializeMap();
170
+ }
171
+ },
172
+ beforeDestroy() {
173
+ if (this.mapInstance) {
174
+ if (!this.hasWebGL && !this.leafletReady) {
175
+ // turn off the leaflet instance
176
+ this.mapInstance.off();
177
+ }
178
+ // remove either leaflet or maplibregl
179
+ this.mapInstance.remove();
180
+ }
181
+ this.destroyWrapperObserver();
182
+ },
183
+ methods: {
184
+ activateZoom(zoomOut = false) {
185
+ const { mapInstance, hasWebGL, mapLibreReady } = this;
186
+ const currentZoomLevel = mapInstance.getZoom();
187
+ // exit if already zoomed in (getZoom() works for both leaflet + maplibregl)
188
+ if ((!zoomOut && currentZoomLevel === this.zoomLevel)
189
+ || (zoomOut && currentZoomLevel === this.initialZoom)) return false;
190
+
191
+ this.zoomActive = true;
192
+ // establish delayed zoom duration
193
+ const timedZoom = window.setTimeout(() => {
194
+ if (hasWebGL && mapLibreReady) {
195
+ // maplibregl specific zoom method
196
+ mapInstance.zoomTo(
197
+ zoomOut ? this.initialZoom : this.zoomLevel,
198
+ { duration: 1200 },
199
+ );
200
+ } else {
201
+ // leaflet specific zoom method
202
+ mapInstance.setZoom(zoomOut ? this.initialZoom : this.zoomLevel);
203
+ }
204
+
205
+ clearTimeout(timedZoom);
206
+ this.zoomActive = false;
207
+ }, this.autoZoomDelay);
208
+ },
209
+ createWrapperObserver() {
210
+ // Watch for the wrapper element moving in and out of the viewport
211
+ this.wrapperObserver = this.createIntersectionObserver({
212
+ targets: [this.$refs?.[this.refString]],
213
+ callback: (entries) => {
214
+ entries.forEach((entry) => {
215
+ if (entry.target === this.$refs?.[this.refString] && !this.zoomActive) {
216
+ if (entry.intersectionRatio > 0) {
217
+ this.activateZoom();
218
+ }
219
+ }
220
+ });
221
+ },
222
+ });
223
+ },
224
+ destroyWrapperObserver() {
225
+ if (this.wrapperObserver) {
226
+ this.wrapperObserver.disconnect();
227
+ }
228
+ },
229
+ checkWebGL() {
230
+ // exit and use leaflet if specified or document isn't present
231
+ if (this.useLeaflet || typeof document === 'undefined') return false;
232
+ // via. https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Detect_WebGL
233
+ // Create canvas element. The canvas is not added to the document itself,
234
+ // so it is never displayed in the browser window.
235
+ const canvas = document.createElement('canvas');
236
+ // Get WebGLRenderingContext from canvas element.
237
+ const gl = canvas.getContext('webgl')
238
+ || canvas.getContext('experimental-webgl');
239
+ // Report the result.
240
+ if (gl && gl instanceof WebGLRenderingContext) {
241
+ this.hasWebGL = true;
242
+ return true;
243
+ }
244
+ return false;
245
+ },
246
+ initializeMap() {
247
+ /**
248
+ * This initial checkWebGL() call kicks off the vue-meta asset inclusion
249
+ * We then start polling for the readiness of our selected map library and initialize it once ready
250
+ */
251
+ if (this.checkWebGL()) {
252
+ this.testDelayedGlobalLibrary('maplibregl').then((response) => {
253
+ if (response.loaded && !this.mapLoaded && !this.useLeaflet && this.lat && this.long) {
254
+ this.initializeMapLibre();
255
+ this.mapLibreReady = true;
256
+ }
257
+ });
258
+ } else {
259
+ this.testDelayedGlobalLibrary('L').then((leafletTest) => {
260
+ if (leafletTest.loaded && !this.mapLoaded && this.lat && this.long) {
261
+ this.initializeLeaflet();
262
+ this.leafletReady = true;
263
+ }
264
+ });
265
+ }
266
+ },
267
+ initializeLeaflet() {
268
+ /* eslint-disable no-undef, max-len */
269
+ // Initialize primary mapInstance
270
+ this.mapInstance = L.map(`kv-map-holder-${this.mapId}`, {
271
+ center: [this.lat, this.long],
272
+ zoom: this.initialZoom || this.zoomLevel,
273
+ // todo make props for the following options
274
+ dragging: false,
275
+ zoomControl: false,
276
+ animate: true,
277
+ scrollWheelZoom: false,
278
+ doubleClickZoom: false,
279
+ attributionControl: false,
280
+ });
281
+ /* eslint-disable quotes */
282
+ // Add our tileset to the mapInstance
283
+ L.tileLayer('https://api.maptiler.com/maps/bright/{z}/{x}/{y}.png?key=n1Mz5ziX3k6JfdjFe7mx', {
284
+ tileSize: 512,
285
+ zoomOffset: -1,
286
+ minZoom: 1,
287
+ crossOrigin: true,
288
+ }).addTo(this.mapInstance);
289
+ /* eslint-enable quotes */
290
+ /* eslint-enable no-undef, max-len */
291
+
292
+ // signify map has loaded
293
+ this.mapLoaded = true;
294
+ // only activate autoZoom if we have an initialZoom set
295
+ if (this.initialZoom !== null) {
296
+ this.createWrapperObserver();
297
+ }
298
+ },
299
+ initializeMapLibre() {
300
+ // Initialize primary mapInstance
301
+ // eslint-disable-next-line no-undef
302
+ this.mapInstance = new maplibregl.Map({
303
+ container: `kv-map-holder-${this.mapId}`,
304
+ style: 'https://api.maptiler.com/maps/bright/style.json?key=n1Mz5ziX3k6JfdjFe7mx',
305
+ center: [this.long, this.lat],
306
+ zoom: this.initialZoom || this.zoomLevel,
307
+ attributionControl: false,
308
+ dragPan: false,
309
+ scrollZoom: false,
310
+ doubleClickZoom: false,
311
+ dragRotate: false,
312
+ });
313
+
314
+ // signify map has loaded
315
+ this.mapLoaded = true;
316
+
317
+ // only activate autoZoom if we have an initialZoom set
318
+ if (this.initialZoom !== null) {
319
+ this.createWrapperObserver();
320
+ }
321
+ },
322
+ checkIntersectionObserverSupport() {
323
+ if (typeof window === 'undefined'
324
+ || !('IntersectionObserver' in window)
325
+ || !('IntersectionObserverEntry' in window)
326
+ || !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
327
+ return false;
328
+ }
329
+ return true;
330
+ },
331
+ createIntersectionObserver({ callback, options, targets } = {}) {
332
+ if (this.checkIntersectionObserverSupport()) {
333
+ const observer = new IntersectionObserver(callback, options);
334
+ targets.forEach((target) => observer.observe(target));
335
+ return observer;
336
+ }
337
+ },
338
+ testDelayedGlobalLibrary(library, timeout = 3000) {
339
+ // return a promise
340
+ return new Promise((resolve, reject) => {
341
+ if (typeof window === 'undefined') {
342
+ reject(new Error('window object not available'));
343
+ }
344
+ // establish timeout to limit time until promise resolution
345
+ let readyStateTimeout;
346
+ // establish interval to check for library presence
347
+ const readyStateInterval = window.setInterval(() => {
348
+ // determine if library is present on window
349
+ if (typeof window[library] !== 'undefined') {
350
+ // cleanup timers
351
+ clearInterval(readyStateInterval);
352
+ clearTimeout(readyStateTimeout);
353
+ // resolve the promise
354
+ resolve({ loaded: true });
355
+ }
356
+ }, 100);
357
+
358
+ // activate timeout
359
+ readyStateTimeout = window.setTimeout(() => {
360
+ // clean up interval and timeout
361
+ clearInterval(readyStateInterval);
362
+ clearTimeout(readyStateTimeout);
363
+ // resolve the promise
364
+ resolve({ loaded: false });
365
+ }, timeout);
366
+ });
367
+ },
368
+ },
369
+ };
370
+ </script>
@@ -0,0 +1,69 @@
1
+ import KvMap from '../KvMap.vue';
2
+
3
+ export default {
4
+ title: 'KvMap',
5
+ component: KvMap,
6
+ args: {
7
+ autoZoomDelay: 1000,
8
+ height: null,
9
+ initialZoom: null,
10
+ lat: 37.700091,
11
+ long: -123.013243,
12
+ mapId: 0,
13
+ useLeaflet: false,
14
+ width: null,
15
+ zoomLevel: 4,
16
+ },
17
+ };
18
+
19
+ const Template = (args, { argTypes }) => ({
20
+ props: Object.keys(argTypes),
21
+ components: { KvMap },
22
+ template: `<kv-map
23
+ class="tw-rounded tw-overflow-hidden"
24
+ :auto-zoom-delay="autoZoomDelay"
25
+ :height="height"
26
+ :lat="lat"
27
+ :long="long"
28
+ :initial-zoom="initialZoom"
29
+ :map-id="mapId"
30
+ :use-leaflet="useLeaflet"
31
+ :width="width"
32
+ :zoom-level="zoomLevel"
33
+ />`,
34
+ });
35
+
36
+ export const Default = Template.bind({});
37
+ Default.args = {
38
+ initialZoom: null,
39
+ mapId: 1,
40
+ zoomLevel: 14,
41
+ };
42
+
43
+ export const AutoZoom = Template.bind({});
44
+ AutoZoom.args = {
45
+ initialZoom: 1,
46
+ lat: -0.023559,
47
+ long: 37.906193,
48
+ mapId: 2,
49
+ zoomLevel: 4,
50
+ };
51
+
52
+ export const FixedDimensions = Template.bind({});
53
+ FixedDimensions.args = {
54
+ initialZoom: null,
55
+ height: 250,
56
+ mapId: 3,
57
+ width: 250,
58
+ zoomLevel: 14,
59
+ };
60
+
61
+ export const Leaflet = Template.bind({});
62
+ Leaflet.args = {
63
+ initialZoom: null,
64
+ lat: -0.023559,
65
+ long: 37.906193,
66
+ mapId: 4,
67
+ useLeaflet: true,
68
+ zoomLevel: 6,
69
+ };