diff --git a/examples/snowbox/index.js b/examples/snowbox/index.js index 764b14f1e5..0b728ab7b6 100644 --- a/examples/snowbox/index.js +++ b/examples/snowbox/index.js @@ -14,6 +14,7 @@ import pluginFilter from '@polar/polar/plugins/filter' import pluginFullscreen from '@polar/polar/plugins/fullscreen' import pluginGeoLocation from '@polar/polar/plugins/geoLocation' import pluginIconMenu from '@polar/polar/plugins/iconMenu' +import pluginInitialView from '@polar/polar/plugins/initialView' import pluginLayerChooser from '@polar/polar/plugins/layerChooser' import pluginLoadingIndicator from '@polar/polar/plugins/loadingIndicator' import pluginPins from '@polar/polar/plugins/pins' @@ -500,6 +501,11 @@ addPlugin( disabledOnMobile: true, icon: 'kern-icon-fill--assistant-direction', }, + { + plugin: pluginInitialView({ + renderType: 'iconMenu', + }), + }, ], [ { diff --git a/src/client.ts b/src/client.ts index 6fc6cd943b..6a05ea2480 100644 --- a/src/client.ts +++ b/src/client.ts @@ -15,6 +15,7 @@ import AddressSearch from '@/plugins/addressSearch' import Fullscreen from '@/plugins/fullscreen' import GeoLocation from '@/plugins/geoLocation' import IconMenu from '@/plugins/iconMenu' +import InitialView from '@/plugins/initialView' import LayerChooser from '@/plugins/layerChooser' import LoadingIndicator from '@/plugins/loadingIndicator' import Pins from '@/plugins/pins' @@ -30,6 +31,9 @@ function addPlugins(map: typeof PolarContainer, enabledPlugins: string[]) { layoutTag: 'TOP_RIGHT', menus: [ [ + enabledPlugins.includes('initialView') && { + plugin: InitialView({ renderType: 'iconMenu' }), + }, enabledPlugins.includes('fullscreen') && { plugin: Fullscreen({ renderType: 'iconMenu' }), }, @@ -91,8 +95,8 @@ function addPlugins(map: typeof PolarContainer, enabledPlugins: string[]) { * @param serviceRegister - Service register given as an array, or a URL to fetch this from. * @param mapConfiguration - Configuration options. Only plugins that have a configuration will be created. * To enable a plugin with default configuration, add its key with an empty object. - * The plugins with the ids 'fullscreen', 'geoLocation', 'routing' and 'layerChooser' are added - * to the IconMenu. The IconMenu, Toast, LayerChooser and LoadingIndicator are enabled by default. + * The plugins with the ids 'fullscreen', 'geoLocation', 'routing', 'initialView' and 'layerChooser' + * are added to the IconMenu. The IconMenu, Toast, LayerChooser and LoadingIndicator are enabled by default. * @param modifyServiceRegister - Optionally modify the serviceRegister. This may be useful if a pre-existing register is used. * * @example diff --git a/src/core/stores/index.ts b/src/core/stores/index.ts index 5d55749a4e..f74b05fce0 100644 --- a/src/core/stores/index.ts +++ b/src/core/stores/index.ts @@ -34,9 +34,8 @@ export const useCoreStore = defineStore('core', () => { * The current center coordinates of the map. * * @alpha - * @readonly */ - center: computed(() => mainStore.center), + center: mainStoreRefs.center, /** * Color scheme the client should be using. diff --git a/src/core/types/main.ts b/src/core/types/main.ts index bb4850a518..5f6b66ec9f 100644 --- a/src/core/types/main.ts +++ b/src/core/types/main.ts @@ -7,6 +7,7 @@ import type { FooterPluginOptions } from '@/plugins/footer' import type { FullscreenPluginOptions } from '@/plugins/fullscreen' import type { GeoLocationPluginOptions } from '@/plugins/geoLocation' import type { IconMenuPluginOptions } from '@/plugins/iconMenu' +import type { InitialViewPluginOptions } from '@/plugins/initialView' import type { LoadingIndicatorOptions } from '@/plugins/loadingIndicator' import type { PinsPluginOptions } from '@/plugins/pins' import type { PointerPositionPluginOptions } from '@/plugins/pointerPosition' @@ -343,6 +344,9 @@ export interface MapConfiguration extends MasterportalApiConfiguration { /** Configuration for iconMenu plugin. */ iconMenu?: IconMenuPluginOptions + /** Configuration for initialView plugin. */ + initialView?: InitialViewPluginOptions + /** Configuration for loadingIndicator plugin. */ loadingIndicator?: LoadingIndicatorOptions diff --git a/src/core/types/plugin.ts b/src/core/types/plugin.ts index 28c6eae760..6326e21dc6 100644 --- a/src/core/types/plugin.ts +++ b/src/core/types/plugin.ts @@ -25,6 +25,9 @@ import type { useGeoLocationStore as GeoLocationStore } from '@/plugins/geoLocat import type { PluginId as IconMenuPluginId } from '@/plugins/iconMenu' import type { resourcesEn as IconMenuResources } from '@/plugins/iconMenu/locales' import type { useIconMenuStore as IconMenuStore } from '@/plugins/iconMenu/store' +import type { PluginId as InitialViewPluginId } from '@/plugins/initialView' +import type { resourcesEn as InitialViewResources } from '@/plugins/initialView/locales' +import type { useInitialViewStore as InitialViewStore } from '@/plugins/initialView/store' import type { PluginId as LayerChooserPluginId } from '@/plugins/layerChooser' import type { resourcesEn as LayerChooserResources } from '@/plugins/layerChooser/locales' import type { useLayerChooserStore as LayerChooserStore } from '@/plugins/layerChooser/store' @@ -153,6 +156,7 @@ export type BundledPluginId = | typeof LoadingIndicatorId | typeof PinsPluginId | typeof PointerPositionPluginId + | typeof InitialViewPluginId | typeof ReverseGeocoderPluginId | typeof RoutingPluginId | typeof ScalePluginId @@ -186,6 +190,7 @@ export type BundledPluginStores = typeof PointerPositionPluginId, typeof PointerPositionStore > + | GetPluginStore | GetPluginStore< T, typeof ReverseGeocoderPluginId, @@ -235,6 +240,11 @@ export type BundledPluginLocaleResources = typeof PointerPositionPluginId, typeof PointerPositionResources > + | GetPluginResources< + T, + typeof InitialViewPluginId, + typeof InitialViewResources + > | GetPluginResources | GetPluginResources | GetPluginResources diff --git a/src/plugins/initialView/components/InitialView.ce.vue b/src/plugins/initialView/components/InitialView.ce.vue new file mode 100644 index 0000000000..33fe263cf3 --- /dev/null +++ b/src/plugins/initialView/components/InitialView.ce.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/plugins/initialView/index.ts b/src/plugins/initialView/index.ts new file mode 100644 index 0000000000..cd127be546 --- /dev/null +++ b/src/plugins/initialView/index.ts @@ -0,0 +1,32 @@ +/* eslint-disable tsdoc/syntax */ +/** + * @module @polar/polar/plugins/InitialView + */ +/* eslint-enable tsdoc/syntax */ + +import type { PluginContainer, PolarPluginStore } from '@/core' +import type { InitialViewPluginOptions } from './types' + +import component from './components/InitialView.ce.vue' +import locales from './locales' +import { useInitialViewStore } from './store' +import { PluginId } from './types' + +/** + * Creates a plugin which offers a button to return to the map's start view. + * + * @returns Plugin for use with {@link addPlugin}. + */ +export default function pluginInitialView( + options: InitialViewPluginOptions = {} +): PluginContainer { + return { + id: PluginId, + component, + locales, + storeModule: useInitialViewStore as PolarPluginStore, + options, + } +} + +export * from './types' diff --git a/src/plugins/initialView/locales.ts b/src/plugins/initialView/locales.ts new file mode 100644 index 0000000000..35491f87e5 --- /dev/null +++ b/src/plugins/initialView/locales.ts @@ -0,0 +1,32 @@ +/* eslint-disable tsdoc/syntax */ +/** + * @module locales/plugins/InitialView + */ +/* eslint-enable tsdoc/syntax */ + +import type { Locale } from '@/core' + +export const resourcesDe = { + label: { + return: 'Zurück zur Startansicht', + }, +} as const + +export const resourcesEn = { + label: { + return: 'Return to start view', + }, +} as const + +const locales: Locale[] = [ + { + type: 'de', + resources: resourcesDe, + }, + { + type: 'en', + resources: resourcesEn, + }, +] + +export default locales diff --git a/src/plugins/initialView/store.ts b/src/plugins/initialView/store.ts new file mode 100644 index 0000000000..b808083961 --- /dev/null +++ b/src/plugins/initialView/store.ts @@ -0,0 +1,116 @@ +import type { ComputedRef } from 'vue' +import type { InitialViewPluginOptions } from './types' + +import { defineStore } from 'pinia' +import { computed } from 'vue' + +import { useCoreStore } from '@/core/stores' + +import { PluginId } from './types' + +export const useInitialViewStore = defineStore('plugins/initialView', () => { + const coreStore = useCoreStore() + + const configuration = computed( + () => coreStore.configuration[PluginId] as InitialViewPluginOptions + ) + + const layoutTag = computed(() => configuration.value.layoutTag ?? '') + + const renderType = computed( + () => configuration.value.renderType ?? 'independent' + ) + + const tooltipPosition = computed(() => + renderType.value === 'independent' + ? layoutTag.value.includes('RIGHT') + ? 'left' + : 'right' + : coreStore.getPluginStore('iconMenu')?.layoutTag.includes('RIGHT') + ? 'left' + : 'right' + ) as ComputedRef<'left' | 'right'> + + const startCenter = computed(() => coreStore.configuration.startCenter) + + const startResolution = computed( + () => coreStore.configuration.startResolution + ) + + function returnToInitialView() { + coreStore.center = startCenter.value + const zoom = coreStore.configuration.options.find( + ({ resolution }) => resolution === startResolution.value + ) + if (zoom) { + coreStore.zoom = zoom.zoomLevel + } + } + + function setupPlugin() {} + + function teardownPlugin() {} + + return { + startCenter, + startResolution, + + /** @alpha */ + returnToInitialView, + + /** @internal */ + setupPlugin, + + /** @internal */ + teardownPlugin, + + /** + * Indicates in which direction of the element space is available for a tooltip. + * + * @alpha + * @readonly + */ + tooltipPosition, + } +}) + +let mockCoreStore: unknown + +if (import.meta.vitest) { + const { createPinia, setActivePinia } = await import('pinia') + const { describe, it, expect, vi, beforeEach } = import.meta.vitest + + const mockView = { + setCenter: vi.fn(), + setResolution: vi.fn(), + } + + vi.mock('@/core/stores', () => ({ + useCoreStore: () => mockCoreStore, + })) + + const { useInitialViewStore } = await import('./store') + + describe('InitialView Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + mockCoreStore = { + map: { + getView: vi.fn(() => mockView), + }, + configuration: { + startCenter: [10, 20], + startResolution: 2, + }, + } + }) + + it('should call setCenter and setResolution on returnToInitialView', () => { + const store = useInitialViewStore() + store.returnToInitialView() + expect(mockView.setCenter).toHaveBeenCalledWith([10, 20]) + expect(mockView.setResolution).toHaveBeenCalledWith(2) + }) + }) +} diff --git a/src/plugins/initialView/types.ts b/src/plugins/initialView/types.ts new file mode 100644 index 0000000000..b19e34745b --- /dev/null +++ b/src/plugins/initialView/types.ts @@ -0,0 +1,12 @@ +import type { PluginOptions } from '@/core' + +export const PluginId = 'initialView' as const + +export interface InitialViewPluginOptions extends PluginOptions { + /** + * Defines if the initialView button is rendered independent or as part of the + * icon menu. + * @defaultValue `'independent'` + */ + renderType?: 'independent' | 'iconMenu' +}