Skip to content

Commit b4d46c1

Browse files
authored
Merge pull request #366 from Dataport/vue3/migration-plugin-geo-location
feat(plugins/geoLocation): migrate plugin
2 parents 4c6c0d7 + d937b77 commit b4d46c1

33 files changed

+960
-1226
lines changed

examples/snowbox/GeoLocationMock.ce.vue

Lines changed: 0 additions & 33 deletions
This file was deleted.

examples/snowbox/index.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
updateState,
88
} from '@polar/polar'
99
import pluginFullscreen from '@polar/polar/plugins/fullscreen'
10+
import pluginGeoLocation from '@polar/polar/plugins/geoLocation'
1011
import pluginIconMenu from '@polar/polar/plugins/iconMenu'
1112
import pluginLayerChooser from '@polar/polar/plugins/layerChooser'
1213
import pluginLoadingIndicator from '@polar/polar/plugins/loadingIndicator'
@@ -15,13 +16,13 @@ import EmptyComponent from './EmptyComponent.vue'
1516
import styleJsonUrl from './style.json?url'
1617
import services from './services.js'
1718
import YetAnotherEmptyComponent from './YetAnotherEmptyComponent.vue'
18-
import GeoLocationMockCe from './GeoLocationMock.ce.vue'
1919

2020
const basemapId = '23420'
2121
const basemapGreyId = '23421'
2222
const ausgleichsflaechen = '1454'
2323
const reports = '6059'
2424
const denkmal = 'denkmaelerWMS'
25+
// const hamburgBorder = '1693' // boundary layer for pins / geolocalization
2526

2627
let colorScheme = 'light'
2728
// eslint-disable-next-line no-unused-vars
@@ -243,11 +244,17 @@ addPlugin(
243244
// TODO: Delete the mock plugins including the components once the correct plugins have been implemented
244245
[
245246
{
246-
plugin: {
247-
component: GeoLocationMockCe,
248-
id: 'geoLocationMock',
249-
locales: [],
250-
},
247+
plugin: pluginGeoLocation({
248+
checkLocationInitially: false,
249+
keepCentered: false,
250+
showTooltip: true,
251+
zoomLevel: 7,
252+
// usable when you're in HH or fake your geolocation to HH
253+
/* boundary: {
254+
layerId: hamburgBorder,
255+
onError: 'strict',
256+
}, */
257+
}),
251258
},
252259
],
253260
[

src/core/types/main.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { LayerConfiguration } from './layer'
55
import type { PolarTheme } from './theme'
66
import type { LocaleOverride } from './locales'
77
import type { FullscreenPluginOptions } from '@/plugins/fullscreen'
8+
import type { GeoLocationPluginOptions } from '@/plugins/geoLocation'
89
import type { IconMenuPluginOptions } from '@/plugins/iconMenu'
910
import type { LoadingIndicatorOptions } from '@/plugins/loadingIndicator'
1011
import type { ToastPluginOptions } from '@/plugins/toast'
@@ -293,6 +294,11 @@ export interface MapConfiguration extends MasterportalApiConfiguration {
293294
*/
294295
fullscreen?: FullscreenPluginOptions
295296

297+
/**
298+
* Configuration for geoLocation plugin.
299+
*/
300+
geoLocation?: GeoLocationPluginOptions
301+
296302
/**
297303
* Configuration for iconMenu plugin.
298304
*/

src/core/types/plugin.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import type { PluginId as FullscreenPluginId } from '@/plugins/fullscreen'
77
import type { useFullscreenStore as FullscreenStore } from '@/plugins/fullscreen/store'
88
import type { resourcesEn as FullscreenResources } from '@/plugins/fullscreen/locales'
99

10+
import type { PluginId as GeoLocationPluginId } from '@/plugins/geoLocation'
11+
import type { useGeoLocationStore as GeoLocationStore } from '@/plugins/geoLocation/store'
12+
import type { resourcesEn as GeoLocationResources } from '@/plugins/geoLocation/locales'
13+
1014
import type { PluginId as IconMenuPluginId } from '@/plugins/iconMenu'
1115
import type { useIconMenuStore as IconMenuStore } from '@/plugins/iconMenu/store'
1216
import type { resourcesEn as IconMenuResources } from '@/plugins/iconMenu/locales'
@@ -27,6 +31,38 @@ export interface PluginOptions {
2731
layoutTag?: keyof typeof NineLayoutTag
2832
}
2933

34+
interface BoundaryOptions {
35+
/**
36+
* ID of the vector layer to restrict requests to.
37+
* The layer must contain vectors. This is useful for restricted maps to avoid
38+
* selecting unfit coordinates.
39+
*/
40+
layerId: string
41+
42+
/**
43+
* If the boundary layer check does not work due to loading or configuration
44+
* errors, style `'strict'` will disable the affected feature, and style
45+
* `'permissive'` will act as if no {@link layerId} was set.
46+
*
47+
* @defaultValue 'permissive'
48+
*/
49+
onError?: 'strict' | 'permissive'
50+
51+
/**
52+
* If the boundary layer check does not work due to loading or configuration
53+
* errors, style `'strict'` will disable the affected feature, and style
54+
* `'permissive'` will act as if no boundaryLayerId was set.
55+
* @defaultValue `'permissive'`
56+
*/
57+
}
58+
59+
export interface LayerBoundPluginOptions extends PluginOptions {
60+
/**
61+
* Set to check whether something is within the layer's boundaries.
62+
*/
63+
boundary?: BoundaryOptions
64+
}
65+
3066
export type PolarPluginStore<
3167
T extends {
3268
setupPlugin?: () => void
@@ -40,6 +76,7 @@ export type PolarPluginStore<
4076
/** @internal */
4177
export type BundledPluginId =
4278
| typeof FullscreenPluginId
79+
| typeof GeoLocationPluginId
4380
| typeof IconMenuPluginId
4481
| typeof LayerChooserPluginId
4582
| typeof LoadingIndicatorId
@@ -57,6 +94,7 @@ type GetPluginStore<
5794
/** @internal */
5895
export type BundledPluginStores<T extends BundledPluginId> =
5996
| GetPluginStore<T, typeof FullscreenPluginId, typeof FullscreenStore>
97+
| GetPluginStore<T, typeof GeoLocationPluginId, typeof GeoLocationStore>
6098
| GetPluginStore<T, typeof IconMenuPluginId, typeof IconMenuStore>
6199
| GetPluginStore<T, typeof LayerChooserPluginId, typeof LayerChooserStore>
62100
| GetPluginStore<T, typeof LoadingIndicatorId, typeof LoadingIndicatorStore>
@@ -71,6 +109,11 @@ type GetPluginResources<
71109
/** @internal */
72110
export type BundledPluginLocaleResources<T extends BundledPluginId> =
73111
| GetPluginResources<T, typeof FullscreenPluginId, typeof FullscreenResources>
112+
| GetPluginResources<
113+
T,
114+
typeof GeoLocationPluginId,
115+
typeof GeoLocationResources
116+
>
74117
| GetPluginResources<T, typeof IconMenuPluginId, typeof IconMenuResources>
75118
| GetPluginResources<
76119
T,

src/lib/tooltip.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import i18next from 'i18next'
1+
import i18next, { type TOptions } from 'i18next'
22

3-
type TooltipLocaleKeys = [string, string][]
3+
type TooltipLocaleKeys = [string, string, TOptions?][]
44

55
export interface Tooltip {
66
/** tooltip as a div, bound to inputs */
@@ -14,9 +14,9 @@ const setInnerHtml =
1414
(tooltip: HTMLDivElement, localeKeys: TooltipLocaleKeys) => () =>
1515
(tooltip.innerHTML = localeKeys
1616
.map(
17-
([element, localeKey]) =>
17+
([element, localeKey, options = {}]) =>
1818
// @ts-expect-error | Locale keys are dynamic.
19-
`<${element}>${i18next.t(localeKey)}</${element}>`
19+
`<${element}>${i18next.t(localeKey, options)}</${element}>`
2020
)
2121
.join(''))
2222

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<template>
2+
<PolarIconButton
3+
:class="
4+
layout === 'nineRegions' ? 'polar-plugin-geoLocation-nineRegions' : ''
5+
"
6+
:hint="$t(($) => $.button.tooltip, { ns: PluginId })"
7+
:icon="icon"
8+
:tooltip-position="tooltipPosition"
9+
:disabled="state === 'DISABLED'"
10+
@click="geoLocationStore.locate"
11+
/>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import { storeToRefs } from 'pinia'
16+
import { computed } from 'vue'
17+
import { useGeoLocationStore } from '../store'
18+
import { PluginId } from '../types'
19+
import PolarIconButton from '@/components/PolarIconButton.ce.vue'
20+
import { useCoreStore } from '@/core/stores/export'
21+
22+
const { layout } = storeToRefs(useCoreStore())
23+
const geoLocationStore = useGeoLocationStore()
24+
const { state } = storeToRefs(geoLocationStore)
25+
26+
const icon = computed(() => {
27+
if (state.value === 'LOCATED') {
28+
return 'kern-icon-fill--near-me'
29+
} else if (state.value === 'LOCATABLE') {
30+
return 'kern-icon--near-me'
31+
}
32+
return 'kern-icon--near-me-disabled'
33+
})
34+
const tooltipPosition = computed(() =>
35+
geoLocationStore.configuration.renderType === 'iconMenu'
36+
? undefined
37+
: layout.value === 'standard' ||
38+
geoLocationStore.configuration.layoutTag?.includes('RIGHT')
39+
? 'left'
40+
: 'right'
41+
)
42+
</script>
43+
44+
<style scoped>
45+
.polar-plugin-geoLocation-nineRegions {
46+
margin: 0.5rem;
47+
}
48+
</style>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { createTestingPinia } from '@pinia/testing'
2+
import { mount, type VueWrapper } from '@vue/test-utils'
3+
import { expect, test as _test, vi } from 'vitest'
4+
import { useGeoLocationStore } from '../store'
5+
import GeoLocation from './GeoLocation.ce.vue'
6+
import { mockedT } from '@/test/utils/mockI18n'
7+
8+
// TODO: Finalize tets once useCoreStore works
9+
10+
/* eslint-disable no-empty-pattern */
11+
const test = _test.extend<{
12+
wrapper: VueWrapper
13+
store: ReturnType<typeof useGeoLocationStore>
14+
}>({
15+
wrapper: async ({}, use) => {
16+
const wrapper = mount(GeoLocation, {
17+
global: {
18+
plugins: [createTestingPinia({ createSpy: vi.fn })],
19+
mocks: {
20+
$t: mockedT,
21+
},
22+
},
23+
})
24+
await use(wrapper)
25+
},
26+
store: async ({}, use) => {
27+
const store = useGeoLocationStore()
28+
await use(store)
29+
},
30+
})
31+
/* eslint-enable no-empty-pattern */
32+
33+
test.skip('The button should include a tooltip', ({ wrapper }) => {
34+
const btn = wrapper.find('button')
35+
expect(btn.element.disabled).toBe('false')
36+
expect(btn.find('.polar-tooltip').exists()).toBe(true)
37+
expect(btn.find('.kern-label').exists()).toBe(true)
38+
})
39+
40+
test.skip('The icon of the button should change on click to a filled icon if the user accepts the location request', async ({
41+
wrapper,
42+
}) => {
43+
const btn = wrapper.find('button')
44+
expect(btn.element.disabled).toBe('false')
45+
expect(btn.find('.kern-icon').element.classList).toContain(
46+
'kern-icon--near-me'
47+
)
48+
49+
await btn.trigger('click')
50+
51+
expect(btn.element.disabled).toBe('false')
52+
expect(btn.find('.kern-icon').element.classList).toContain(
53+
'kern-icon--near-me-filled'
54+
)
55+
})
56+
57+
test.skip('The icon of the button should change on click to a disabled icon and be disabled if the user declines the location', async ({
58+
wrapper,
59+
store,
60+
}) => {
61+
const btn = wrapper.find('button')
62+
63+
expect(btn.element.disabled).toBe('false')
64+
expect(btn.find('.kern-icon').element.classList).toContain(
65+
'kern-icon--near-me'
66+
)
67+
68+
store.isGeolocationDenied = true
69+
await btn.trigger('click')
70+
71+
expect(btn.element.disabled).toBe('true')
72+
expect(btn.find('.kern-icon').element.classList).toContain(
73+
'kern-icon--near-me-disabled'
74+
)
75+
})

src/plugins/geoLocation/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* eslint-disable tsdoc/syntax */
2+
/**
3+
* @module \@polar/polar/plugins/geoLocation
4+
*/
5+
/* eslint-enable tsdoc/syntax */
6+
7+
import component from './components/GeoLocation.ce.vue'
8+
import locales from './locales'
9+
import { PluginId, type GeoLocationPluginOptions } from './types'
10+
import { useGeoLocationStore } from './store'
11+
import type { PluginContainer, PolarPluginStore } from '@/core'
12+
13+
/**
14+
* The GeoLocation plugin is responsible for collecting and displaying a user's
15+
* GPS location for display on the map. The tracking can be triggered initially
16+
* on startup or via a button.
17+
*
18+
* If a users denies the location tracking, the button for this plugin gets
19+
* disabled and indicates the user's decision.
20+
*
21+
* @returns Plugin for use with {@link addPlugin}.
22+
*/
23+
export default function pluginGeoLocation(
24+
options: GeoLocationPluginOptions
25+
): PluginContainer {
26+
return {
27+
id: PluginId,
28+
component,
29+
locales,
30+
storeModule: useGeoLocationStore as PolarPluginStore,
31+
options,
32+
}
33+
}
34+
35+
export * from './types'

0 commit comments

Comments
 (0)