kopia lustrzana https://github.com/FacilMap/facilmap
Rearrange controls in narrow mode
rodzic
e585e5917e
commit
e9f64750f5
|
@ -469,7 +469,8 @@
|
|||
},
|
||||
"leaflet-map": {
|
||||
"open-full-size": "{{appName}} als ganze Seite öffnen",
|
||||
"loading": "Wird geladen…"
|
||||
"loading": "Wird geladen…",
|
||||
"attribution-notice": "Diese App basiert auf Kartendaten von [OpenStreetMap](https://www.openstreetmap.org/copyright/de). Mehr Details im Dialog „[Über {{appName}}](#about-dialog)“ im Hilfe-Menü."
|
||||
},
|
||||
"leaflet-map-components": {
|
||||
"pois-too-many-results": "Nicht alle POIs konnten geladen werden, weil zu viele gefunden wurden. Zoom Sie weiter hinein, um alle POIs anzuzeigen.",
|
||||
|
|
|
@ -471,7 +471,8 @@
|
|||
},
|
||||
"leaflet-map": {
|
||||
"open-full-size": "Open {{appName}} in full size",
|
||||
"loading": "Loading…"
|
||||
"loading": "Loading…",
|
||||
"attribution-notice": "This app is based on map data from [OpenStreetMap](https://www.openstreetmap.org/copyright). Find out more in the “[About {{appName}}](#about-dialog)” dialog in the Help menu."
|
||||
},
|
||||
"leaflet-map-components": {
|
||||
"pois-too-many-results": "Not all POIs are shown because there are too many results. Zoom in to show all results.",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { type InjectionKey, type Ref, inject, onScopeDispose, provide, shallowReactive, toRef, watch, reactive, readonly, shallowReadonly } from "vue";
|
||||
import { useMaxBreakpoint } from "../../utils/bootstrap";
|
||||
import { useIsNarrow } from "../../utils/bootstrap";
|
||||
import type { FacilMapComponents, FacilMapContext, FacilMapSettings } from "./facil-map-context";
|
||||
|
||||
const contextInject = Symbol("contextInject") as InjectionKey<FacilMapContext>;
|
||||
|
@ -45,7 +45,7 @@
|
|||
appName: "FacilMap"
|
||||
});
|
||||
|
||||
const isNarrow = useMaxBreakpoint("sm");
|
||||
const isNarrow = useIsNarrow();
|
||||
|
||||
const components = shallowReactive<FacilMapComponents>({});
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ export type MapContextEvents = {
|
|||
};
|
||||
|
||||
export interface MapComponents {
|
||||
zoomControl: L.Control.Zoom;
|
||||
attribution: AttributionControl;
|
||||
bboxHandler: BboxHandler;
|
||||
container: HTMLElement;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type Ref, ref, watch, markRaw, reactive, watchEffect, shallowRef, shallowReadonly, type Raw, nextTick } from "vue";
|
||||
import { type Control, latLng, latLngBounds, type Map, map as leafletMap, DomUtil, control } from "leaflet";
|
||||
import { type Ref, ref, watch, markRaw, reactive, watchEffect, shallowRef, shallowReadonly, type Raw, nextTick, effectScope, onScopeDispose } from "vue";
|
||||
import { Control, latLng, latLngBounds, type Map, map as leafletMap, DomUtil, control } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { BboxHandler, getIconHtml, getVisibleLayers, HashHandler, LinesLayer, MarkersLayer, SearchResultsLayer, OverpassLayer, OverpassLoadStatus, displayView, getInitialView, coreIconList } from "facilmap-leaflet";
|
||||
import "leaflet.locatecontrol";
|
||||
|
@ -18,17 +18,20 @@ import { requireClientContext } from "../facil-map-context-provider/facil-map-co
|
|||
import { type Optional } from "facilmap-utils";
|
||||
import { getI18n, i18nResourceChangeCounter } from "../../utils/i18n";
|
||||
import { AttributionControl } from "./attribution";
|
||||
import { fixOnCleanup } from "../../utils/vue";
|
||||
import { isMaxBreakpoint, isNarrowBreakpoint } from "../../utils/bootstrap";
|
||||
|
||||
type MapContextWithoutComponents = Optional<WritableMapContext, 'components'>;
|
||||
type OnCleanup = (cleanupFn: () => void) => void;
|
||||
|
||||
function useMap(element: Ref<HTMLElement>, mapContext: MapContextWithoutComponents): Ref<Raw<Map>> {
|
||||
const mapRef = shallowRef(undefined as any as Map);
|
||||
const interaction = ref(0);
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
const map = mapRef.value = markRaw(leafletMap(element.value, { boxZoom: false, attributionControl: false }));
|
||||
const map = mapRef.value = markRaw(leafletMap(element.value, {
|
||||
boxZoom: false,
|
||||
attributionControl: false,
|
||||
zoomControl: false
|
||||
}));
|
||||
|
||||
map._controlCorners.bottomcenter = DomUtil.create("div", "leaflet-bottom fm-leaflet-center", map._controlContainer);
|
||||
|
||||
|
@ -80,7 +83,7 @@ function useMap(element: Ref<HTMLElement>, mapContext: MapContextWithoutComponen
|
|||
function useMapComponent<T>(
|
||||
map: Ref<Map>,
|
||||
construct: () => T,
|
||||
activate: (component: T, onCleanup: OnCleanup) => void
|
||||
activate: (component: T, map: Map) => void
|
||||
): Ref<T> {
|
||||
const componentRef = shallowRef(undefined as any as T);
|
||||
watchEffect(() => {
|
||||
|
@ -90,26 +93,56 @@ function useMapComponent<T>(
|
|||
watch([
|
||||
componentRef,
|
||||
map
|
||||
], ([component], prev, onCleanup_) => {
|
||||
const onCleanup = fixOnCleanup(onCleanup_);
|
||||
activate(component as T, onCleanup);
|
||||
], ([component, map], prev, onCleanup) => {
|
||||
const scope = effectScope();
|
||||
scope.run(() => {
|
||||
activate(component as any, map);
|
||||
});
|
||||
onCleanup(() => {
|
||||
scope.stop();
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
return componentRef;
|
||||
}
|
||||
|
||||
function useZoomControl(map: Ref<Map>): Ref<Raw<Control.Zoom>> {
|
||||
return useMapComponent(
|
||||
map,
|
||||
() => markRaw(control.zoom()),
|
||||
(zoomControl, map) => {
|
||||
watch(() => isNarrowBreakpoint(), (isNarrow) => {
|
||||
zoomControl.setPosition(isNarrow ? "bottomright" : "topleft");
|
||||
}, { immediate: true });
|
||||
map.addControl(zoomControl);
|
||||
|
||||
onScopeDispose(() => {
|
||||
zoomControl.remove();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function useAttribution(map: Ref<Map>): Ref<Raw<AttributionControl>> {
|
||||
return useMapComponent(
|
||||
map,
|
||||
() => markRaw(new AttributionControl()),
|
||||
(attribution, onCleanup) => {
|
||||
map.value.addControl(attribution);
|
||||
(attribution, map) => {
|
||||
watch(() => isNarrowBreakpoint(), (isNarrow) => {
|
||||
if (isNarrow) {
|
||||
attribution.remove();
|
||||
} else {
|
||||
map.addControl(attribution);
|
||||
}
|
||||
//attribution.setPosition(isNarrow ? "topleft" : "bottomright");
|
||||
}, { immediate: true });
|
||||
|
||||
const i18nWatcher = watch(i18nResourceChangeCounter, () => {
|
||||
watch(i18nResourceChangeCounter, () => {
|
||||
attribution.update();
|
||||
});
|
||||
onCleanup(() => {
|
||||
i18nWatcher();
|
||||
|
||||
onScopeDispose(() => {
|
||||
attribution.remove();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -119,9 +152,9 @@ function useBboxHandler(map: Ref<Map>, client: Ref<ClientContext>): Ref<Raw<Bbox
|
|||
return useMapComponent(
|
||||
map,
|
||||
() => markRaw(new BboxHandler(map.value, client.value)),
|
||||
(bboxHandler, onCleanup) => {
|
||||
(bboxHandler) => {
|
||||
bboxHandler.enable();
|
||||
onCleanup(() => {
|
||||
onScopeDispose(() => {
|
||||
bboxHandler.disable();
|
||||
});
|
||||
}
|
||||
|
@ -132,9 +165,16 @@ function useGraphicScale(map: Ref<Map>): Ref<Raw<any>> {
|
|||
return useMapComponent(
|
||||
map,
|
||||
() => markRaw(control.graphicScale({ fill: "hollow", position: "bottomcenter" })),
|
||||
(graphicScale, onCleanup) => {
|
||||
graphicScale.addTo(map.value);
|
||||
onCleanup(() => {
|
||||
(graphicScale, map) => {
|
||||
watch(() => isNarrowBreakpoint(), (isNarrow) => {
|
||||
if (isNarrow) {
|
||||
graphicScale.remove();
|
||||
} else {
|
||||
graphicScale.addTo(map);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
onScopeDispose(() => {
|
||||
graphicScale.remove();
|
||||
});
|
||||
}
|
||||
|
@ -145,9 +185,9 @@ function useLinesLayer(map: Ref<Map>, client: Ref<ClientContext>): Ref<Raw<Lines
|
|||
return useMapComponent(
|
||||
map,
|
||||
() => markRaw(new LinesLayer(client.value)),
|
||||
(linesLayer, onCleanup) => {
|
||||
linesLayer.addTo(map.value);
|
||||
onCleanup(() => {
|
||||
(linesLayer, map) => {
|
||||
linesLayer.addTo(map);
|
||||
onScopeDispose(() => {
|
||||
linesLayer.remove();
|
||||
});
|
||||
}
|
||||
|
@ -159,10 +199,14 @@ function useLocateControl(map: Ref<Map>, context: FacilMapContext): Ref<Raw<Cont
|
|||
map,
|
||||
() => {
|
||||
if (context.settings.locate) {
|
||||
if (!coreIconList.includes("screenshot")) {
|
||||
console.warn(`Icon "screenshot" is not in core icons.`);
|
||||
}
|
||||
|
||||
let screenshotIconHtmlP = getIconHtml("currentColor", "1.5em", "screenshot");
|
||||
|
||||
return markRaw(control.locate({
|
||||
flyTo: true,
|
||||
icon: "a",
|
||||
iconLoading: "a",
|
||||
markerStyle: { pane: "fm-raised-marker", zIndexOffset: 10000 },
|
||||
locateOptions: {
|
||||
enableHighAccuracy: true
|
||||
|
@ -171,25 +215,29 @@ function useLocateControl(map: Ref<Map>, context: FacilMapContext): Ref<Raw<Cont
|
|||
inView: "stop",
|
||||
outOfView: "setView",
|
||||
inViewNotFollowing: "outOfView"
|
||||
},
|
||||
createButtonCallback: (container, options) => {
|
||||
const { link, icon } = (Control.Locate.prototype.options as Control.LocateOptions).createButtonCallback!(container, options) as any as { link: HTMLElement; icon: HTMLElement };
|
||||
icon.remove();
|
||||
const newIcon = document.createElement("span");
|
||||
link.appendChild(newIcon);
|
||||
screenshotIconHtmlP.then((iconHtml) => {
|
||||
newIcon.innerHTML = iconHtml;
|
||||
}).catch(console.error);
|
||||
return { link, icon: newIcon };
|
||||
}
|
||||
}));
|
||||
}
|
||||
},
|
||||
(locateControl, onCleanup) => {
|
||||
(locateControl, map) => {
|
||||
if (locateControl) {
|
||||
locateControl.addTo(map.value);
|
||||
watch(() => isNarrowBreakpoint(), (isNarrow) => {
|
||||
locateControl.setPosition(isNarrow ? "bottomright" : "topleft");
|
||||
}, { immediate: true });
|
||||
|
||||
if (!coreIconList.includes("screenshot")) {
|
||||
console.warn(`Icon "screenshot" is not in core icons.`);
|
||||
}
|
||||
locateControl.addTo(map);
|
||||
|
||||
getIconHtml("currentColor", "1.5em", "screenshot").then((html) => {
|
||||
locateControl._container.querySelector("a")?.insertAdjacentHTML("beforeend", html);
|
||||
}).catch((err) => {
|
||||
console.error("Error loading locate control icon", err);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
onScopeDispose(() => {
|
||||
locateControl.remove();
|
||||
});
|
||||
}
|
||||
|
@ -201,9 +249,9 @@ function useMarkersLayer(map: Ref<Map>, client: Ref<ClientContext>): Ref<Raw<Mar
|
|||
return useMapComponent(
|
||||
map,
|
||||
() => markRaw(new MarkersLayer(client.value)),
|
||||
(markersLayer, onCleanup) => {
|
||||
markersLayer.addTo(map.value);
|
||||
onCleanup(() => {
|
||||
(markersLayer, map) => {
|
||||
markersLayer.addTo(map);
|
||||
onScopeDispose(() => {
|
||||
markersLayer.remove();
|
||||
});
|
||||
}
|
||||
|
@ -214,9 +262,16 @@ function useMousePosition(map: Ref<Map>): Ref<Raw<Control.MousePosition>> {
|
|||
return useMapComponent(
|
||||
map,
|
||||
() => markRaw(control.mousePosition({ emptyString: "0, 0", separator: ", ", position: "bottomright" })),
|
||||
(mousePosition, onCleanup) => {
|
||||
mousePosition.addTo(map.value);
|
||||
onCleanup(() => {
|
||||
(mousePosition, map) => {
|
||||
watch(() => isNarrowBreakpoint(), (isNarrow) => {
|
||||
if (isNarrow) {
|
||||
mousePosition.remove();
|
||||
} else {
|
||||
mousePosition.addTo(map);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
onScopeDispose(() => {
|
||||
mousePosition.remove();
|
||||
});
|
||||
}
|
||||
|
@ -252,9 +307,9 @@ function useOverpassLayer(map: Ref<Map>, mapContext: MapContextWithoutComponents
|
|||
.on("clear", () => {
|
||||
mapContext.overpassMessage = undefined;
|
||||
}),
|
||||
(overpassLayer, onCleanup) => {
|
||||
overpassLayer.addTo(map.value)
|
||||
onCleanup(() => {
|
||||
(overpassLayer, map) => {
|
||||
overpassLayer.addTo(map)
|
||||
onScopeDispose(() => {
|
||||
overpassLayer.remove();
|
||||
});
|
||||
}
|
||||
|
@ -265,9 +320,9 @@ function useSearchResultsLayer(map: Ref<Map>): Ref<Raw<SearchResultsLayer>> {
|
|||
return useMapComponent(
|
||||
map,
|
||||
() => markRaw(new SearchResultsLayer(undefined, { pathOptions: { weight: 7 } })),
|
||||
(searchResultsLayer, onCleanup) => {
|
||||
searchResultsLayer.addTo(map.value);
|
||||
onCleanup(() => {
|
||||
(searchResultsLayer, map) => {
|
||||
searchResultsLayer.addTo(map);
|
||||
onScopeDispose(() => {
|
||||
searchResultsLayer.remove();
|
||||
});
|
||||
}
|
||||
|
@ -301,9 +356,9 @@ function useSelectionHandler(map: Ref<Map>, context: FacilMapContext, mapContext
|
|||
|
||||
return selectionHandler;
|
||||
},
|
||||
(selectionHandler, onCleanup) => {
|
||||
(selectionHandler) => {
|
||||
selectionHandler.enable();
|
||||
onCleanup(() => {
|
||||
onScopeDispose(() => {
|
||||
selectionHandler.disable();
|
||||
});
|
||||
}
|
||||
|
@ -339,8 +394,8 @@ function useHashHandler(map: Ref<Map>, client: Ref<ClientContext>, context: Faci
|
|||
}
|
||||
});
|
||||
},
|
||||
(hashHandler, onCleanup) => {
|
||||
onCleanup(() => {
|
||||
(hashHandler) => {
|
||||
onScopeDispose(() => {
|
||||
hashHandler.disable();
|
||||
});
|
||||
}
|
||||
|
@ -351,6 +406,7 @@ function useMapComponents(context: FacilMapContext, mapContext: MapContextWithou
|
|||
const client = requireClientContext(context);
|
||||
const map = useMap(mapRef, mapContext);
|
||||
const attribution = useAttribution(map);
|
||||
const zoomControl = useZoomControl(map);
|
||||
const bboxHandler = useBboxHandler(map, client);
|
||||
const graphicScale = useGraphicScale(map);
|
||||
const linesLayer = useLinesLayer(map, client);
|
||||
|
@ -364,6 +420,7 @@ function useMapComponents(context: FacilMapContext, mapContext: MapContextWithou
|
|||
|
||||
const components: MapComponents = reactive({
|
||||
map,
|
||||
zoomControl,
|
||||
attribution,
|
||||
bboxHandler,
|
||||
graphicScale,
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
import vTooltip from "../../utils/tooltip";
|
||||
import type { WritableMapContext } from "../facil-map-context-provider/map-context";
|
||||
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import { useI18n } from "../../utils/i18n";
|
||||
import { useI18n, vReplaceLinks } from "../../utils/i18n";
|
||||
import AboutDialog from "../about-dialog.vue";
|
||||
import { markdownInline } from "facilmap-utils";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const client = requireClientContext(context);
|
||||
|
@ -15,6 +17,8 @@
|
|||
|
||||
const loaded = ref(false);
|
||||
const fatalError = ref<string>();
|
||||
const showNarrowAttribution = ref(true);
|
||||
const aboutDialogOpen = ref(false);
|
||||
|
||||
const selfUrl = computed(() => {
|
||||
return `${location.origin}${location.pathname}${mapContext.value?.hash ? `#${mapContext.value.hash}` : ''}`;
|
||||
|
@ -26,6 +30,9 @@
|
|||
try {
|
||||
mapContext.value = await useMapContext(context, mapRef as Ref<HTMLElement>, innerContainerRef as Ref<HTMLElement>);
|
||||
loaded.value = true;
|
||||
setTimeout(() => {
|
||||
showNarrowAttribution.value = false;
|
||||
}, 5000); // According to https://osmfoundation.org/wiki/Licence/Attribution_Guidelines#Interactive_maps we can fade out the attribution after 5 seconds
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
fatalError.value = err.message;
|
||||
|
@ -43,6 +50,19 @@
|
|||
<div class="fm-leaflet-map-inner-container" ref="innerContainerRef">
|
||||
<div class="fm-leaflet-map" ref="mapRef"></div>
|
||||
|
||||
<div
|
||||
class="fm-leaflet-map-narrow-attribution"
|
||||
:class="{ visible: showNarrowAttribution }"
|
||||
v-html="markdownInline(i18n.t('leaflet-map.attribution-notice', { appName: context.appName }), true)"
|
||||
v-replace-links="{
|
||||
'#about-dialog': { onClick: () => { aboutDialogOpen = true; } }
|
||||
}"
|
||||
></div>
|
||||
<AboutDialog
|
||||
v-if="aboutDialogOpen"
|
||||
@hidden="aboutDialogOpen = false"
|
||||
></AboutDialog>
|
||||
|
||||
<div v-if="mapContext && mapContext.overpassMessage" class="alert alert-warning fm-overpass-message">
|
||||
{{mapContext.overpassMessage}}
|
||||
</div>
|
||||
|
@ -118,6 +138,10 @@
|
|||
pointer-events: none;
|
||||
padding-right: 0;
|
||||
|
||||
// Make font size the same as attribution control
|
||||
font-size: inherit;
|
||||
line-height: 1.4;
|
||||
|
||||
&:after {
|
||||
content: " |";
|
||||
}
|
||||
|
@ -162,8 +186,47 @@
|
|||
}
|
||||
|
||||
&.isNarrow {
|
||||
.leaflet-control-graphicscale,.leaflet-control-mouseposition {
|
||||
display: none !important;
|
||||
.leaflet-control-locate {
|
||||
float: none;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 44px;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom {
|
||||
border: none;
|
||||
|
||||
.leaflet-control-zoom-in {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom-in,.leaflet-control-zoom-out {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 4px;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fm-leaflet-map-narrow-attribution {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
max-width: calc(100% - 54px);
|
||||
opacity: 0;
|
||||
transition: opacity 1s;
|
||||
|
||||
// Style like attribution control
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
font-size: 0.75rem;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import Sidebar from "../ui/sidebar.vue";
|
||||
import Icon from "../ui/icon.vue";
|
||||
import { ref } from "vue";
|
||||
import { ref, toRef, useCssModule, watchEffect } from "vue";
|
||||
import ToolboxAddDropdown from "./toolbox-add-dropdown.vue";
|
||||
import ToolboxCollabMapsDropdown from "./toolbox-collab-maps-dropdown.vue";
|
||||
import ToolboxHelpDropdown from "./toolbox-help-dropdown.vue";
|
||||
|
@ -9,8 +9,20 @@
|
|||
import ToolboxToolsDropdown from "./toolbox-tools-dropdown.vue";
|
||||
import ToolboxViewsDropdown from "./toolbox-views-dropdown.vue";
|
||||
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import { isNarrowBreakpoint } from "../../utils/bootstrap";
|
||||
import { fixOnCleanup } from "../../utils/vue";
|
||||
import { Control, DomUtil, type Map } from "leaflet";
|
||||
|
||||
class CustomControl extends Control {
|
||||
override onAdd(map: Map) {
|
||||
return DomUtil.create("div", "leaflet-bar");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const context = injectContextRequired();
|
||||
const mapContext = toRef(() => context.components.map);
|
||||
const client = requireClientContext(context);
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
@ -19,18 +31,39 @@
|
|||
interactive: true
|
||||
});
|
||||
|
||||
const styles = useCssModule();
|
||||
|
||||
const sidebarVisible = ref(false);
|
||||
|
||||
const menuButtonContainerRef = ref<HTMLElement>();
|
||||
|
||||
watchEffect((onCleanup_) => {
|
||||
const onCleanup = fixOnCleanup(onCleanup_);
|
||||
|
||||
if (isNarrowBreakpoint() && mapContext.value) {
|
||||
const customControl = new CustomControl({ position: "topright" });
|
||||
customControl.addTo(mapContext.value.components.map);
|
||||
customControl._container.classList.add(styles["toggle-container"]);
|
||||
menuButtonContainerRef.value = customControl._container;
|
||||
|
||||
onCleanup(() => {
|
||||
menuButtonContainerRef.value = undefined;
|
||||
customControl.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fm-toolbox">
|
||||
<a
|
||||
v-if="context.isNarrow"
|
||||
v-show="!sidebarVisible"
|
||||
href="javascript:"
|
||||
class="fm-toolbox-toggle"
|
||||
@click="sidebarVisible = true"
|
||||
><Icon icon="menu-hamburger"></Icon></a>
|
||||
<Teleport v-if="menuButtonContainerRef" :to="menuButtonContainerRef">
|
||||
<a
|
||||
v-show="!sidebarVisible"
|
||||
href="javascript:"
|
||||
class="fm-toolbox-toggle"
|
||||
@click="sidebarVisible = true"
|
||||
><Icon icon="menu-hamburger" size="1.5em"></Icon></a>
|
||||
</Teleport>
|
||||
|
||||
<Sidebar :id="`fm${context.id}-toolbox-sidebar`" v-model:visible="sidebarVisible">
|
||||
<ul class="navbar-nav">
|
||||
|
@ -75,22 +108,6 @@
|
|||
z-index: 1000;
|
||||
}
|
||||
|
||||
.fm-toolbox-toggle {
|
||||
color: #444;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: #f4f4f4;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-sidebar.isNarrow {
|
||||
.navbar-nav {
|
||||
max-width: 100%;
|
||||
|
@ -124,4 +141,12 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" module>
|
||||
.toggle-container > a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { type SlotsType, computed, defineComponent, h, ref, shallowRef, useSlots, watch, watchEffect } from "vue";
|
||||
import { getMaxSizeModifiers, type ButtonSize, type ButtonVariant, useMaxBreakpoint } from "../../utils/bootstrap";
|
||||
import { getMaxSizeModifiers, type ButtonSize, type ButtonVariant, useIsNarrow } from "../../utils/bootstrap";
|
||||
import Dropdown from "bootstrap/js/dist/dropdown";
|
||||
import vLinkDisabled from "../../utils/link-disabled";
|
||||
import type { TooltipPlacement } from "../../utils/tooltip";
|
||||
|
@ -42,7 +42,7 @@
|
|||
const buttonRef = ref<InstanceType<typeof AttributePreservingElement>>();
|
||||
const dropdownRef = shallowRef<Dropdown>();
|
||||
|
||||
const isNarrow = useMaxBreakpoint("sm");
|
||||
const isNarrow = useIsNarrow();
|
||||
|
||||
class CustomDropdown extends Dropdown {
|
||||
_detectNavbar() {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import { onMounted, ref, watchEffect } from "vue";
|
||||
import { useRefWithOverride } from "../../utils/vue";
|
||||
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import { getUniqueId } from "../../utils/utils";
|
||||
|
||||
const context = injectContextRequired();
|
||||
|
||||
|
|
|
@ -35,6 +35,10 @@ export function isMaxBreakpoint(breakpoint: Breakpoint): boolean {
|
|||
return breakpoints.indexOf(reactiveBreakpoint.value) <= breakpoints.indexOf(breakpoint);
|
||||
}
|
||||
|
||||
export function isNarrowBreakpoint(): boolean {
|
||||
return isMaxBreakpoint("sm");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reactive boolean that is true if the current breakpoint is the specified one or smaller.
|
||||
*/
|
||||
|
@ -42,6 +46,10 @@ export function useMaxBreakpoint(breakpoint: Breakpoint): Ref<boolean> {
|
|||
return computed(() => isMaxBreakpoint(breakpoint));
|
||||
}
|
||||
|
||||
export function useIsNarrow(): Ref<boolean> {
|
||||
return computed(() => isNarrowBreakpoint());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reactive boolean that is true if the current breakpoint is the specified one or larger.
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="vite/client" />
|
||||
import { type i18n } from "i18next";
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { defineComponent, ref, type Directive } from "vue";
|
||||
import messagesEn from "../../i18n/en.json";
|
||||
import messagesDe from "../../i18n/de.json";
|
||||
import messagesNbNo from "../../i18n/nb-NO.json";
|
||||
|
@ -98,6 +98,26 @@ export const T = defineComponent({
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Replaces all descendent links of the element that match the given URLs with the given link configuration. This allows
|
||||
* replacing links in translation texts using markdown with custom functionality (such as opening a dialog).
|
||||
*/
|
||||
export const vReplaceLinks: Directive<HTMLElement, Record<string, { onClick: (e: Event) => void }>> = (el, binding) => {
|
||||
for (const link of el.querySelectorAll("a[href]")) {
|
||||
const href = link.getAttribute("href");
|
||||
if (binding.value[href!]) {
|
||||
link.setAttribute("href", "javascript:");
|
||||
link.removeAttribute("target");
|
||||
|
||||
link.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
binding.value[href!].onClick(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function isLanguageExplicit(): boolean {
|
||||
const queryParams = decodeQueryString(location.search);
|
||||
return !!queryParams[LANG_QUERY] || !!queryParams[LANG_COOKIE];
|
||||
|
|
|
@ -114,12 +114,12 @@ export function createDefaultLayers(): Layers & { fallbackLayer: string | undefi
|
|||
noWrap: true
|
||||
})),
|
||||
|
||||
Rlie: fixAttribution(tileLayer("https://tiles.wmflabs.org/hillshading/{z}/{x}/{y}.png", {
|
||||
Rlie: tileLayer("https://tiles.wmflabs.org/hillshading/{z}/{x}/{y}.png", {
|
||||
maxZoom: 16,
|
||||
...fmName(() => getI18n().t("layers.rlie-name")),
|
||||
zIndex: 300,
|
||||
noWrap: true
|
||||
})),
|
||||
}),
|
||||
|
||||
grid: new AutoGraticule({
|
||||
...fmName(() => getI18n().t("layers.grid-name")),
|
||||
|
|
Ładowanie…
Reference in New Issue