kopia lustrzana https://github.com/FacilMap/facilmap
pull/256/head
rodzic
0d4760bf4b
commit
a7b7693b43
|
@ -36,12 +36,12 @@
|
|||
"socket.io-client": "^4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/geojson": "^7946.0.11",
|
||||
"@types/rollup-plugin-auto-external": "^2.0.3",
|
||||
"@types/geojson": "^7946.0.13",
|
||||
"@types/rollup-plugin-auto-external": "^2.0.5",
|
||||
"rimraf": "^5.0.5",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-dts": "^3.6.0"
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-dts": "^3.6.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,14 +40,11 @@
|
|||
"dependencies": {
|
||||
"@ckpack/vue-color": "^1.5.0",
|
||||
"@tmcw/togeojson": "^5.8.1",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vitejs/plugin-vue": "^4.4.1",
|
||||
"blob": "^0.1.0",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap-touchspin": "^4.7.3",
|
||||
"clipboard": "^2.0.11",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"decode-uri-component": "^0.4.1",
|
||||
"domutils": "^3.1.0",
|
||||
"facilmap-client": "workspace:^",
|
||||
"facilmap-leaflet": "workspace:^",
|
||||
"facilmap-types": "workspace:^",
|
||||
|
@ -55,10 +52,9 @@
|
|||
"file-saver": "^2.0.5",
|
||||
"hammerjs": "^2.0.8",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-ui": "^1.13.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-draggable-lines": "^1.2.1",
|
||||
"leaflet-graphicscale": "^0.0.2",
|
||||
"leaflet-graphicscale": "^0.0.4",
|
||||
"leaflet-mouse-position": "^1.2.0",
|
||||
"leaflet.heightgraph": "^1.4.0",
|
||||
"leaflet.locatecontrol": "^0.79.0",
|
||||
|
@ -71,41 +67,29 @@
|
|||
"popper-max-size-modifier": "^0.2.0",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"tablesorter": "^2.31.3",
|
||||
"vite": "^4.4.11",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-css-injected-by-js": "^3.3.0",
|
||||
"vite-plugin-dts": "^3.6.0",
|
||||
"vue": "^3.3.4",
|
||||
"vite-plugin-dts": "^3.6.3",
|
||||
"vue": "^3.3.8",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "^5.2.9",
|
||||
"@types/decode-uri-component": "^0.2.0",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/hammerjs": "^2.0.42",
|
||||
"@types/jquery": "^3.5.21",
|
||||
"@types/leaflet": "^1.9.6",
|
||||
"@types/leaflet-mouse-position": "^1.2.2",
|
||||
"@types/leaflet.locatecontrol": "^0.74.2",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/pluralize": "^0.0.31",
|
||||
"@types/scrollparent": "^2.0.1",
|
||||
"css-loader": "^6.8.1",
|
||||
"ejs-compiled-loader": "^3.1.0",
|
||||
"happy-dom": "^12.9.1",
|
||||
"html-loader": "^4.2.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"@types/decode-uri-component": "^0.2.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/hammerjs": "^2.0.44",
|
||||
"@types/jquery": "^3.5.27",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/leaflet-mouse-position": "^1.2.4",
|
||||
"@types/leaflet.locatecontrol": "^0.74.4",
|
||||
"@types/lodash-es": "^4.17.11",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"happy-dom": "^12.10.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"sass": "^1.68.0",
|
||||
"sass-loader": "^13.3.2",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"svgo": "^3.0.2",
|
||||
"ts-loader": "^9.4.4",
|
||||
"svgo": "^3.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vitest": "^0.34.6",
|
||||
"vue-template-compiler": "^2.7.14",
|
||||
"vue-template-loader": "^1.1.0",
|
||||
"vue-tsc": "^1.8.22"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
>
|
||||
<p><a href="https://github.com/facilmap/facilmap" target="_blank"><strong>FacilMap</strong></a> is available under the <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">GNU Affero General Public License, Version 3</a>.</p>
|
||||
<p>If something does not work or you have a suggestion for improvement, please report on the <a href="https://github.com/FacilMap/facilmap/issues" target="_blank">issue tracker</a>.</p>
|
||||
<p>If you have a question, please have a look at the <a href="https://docs.facilmap.org/users/">documentation</a> or raise a question in the <a href="https://github.com/FacilMap/facilmap/discussions">discussion forum</a>.</p>
|
||||
<p><a href="https://docs.facilmap.org/users/privacy/">Privacy information</a></p>
|
||||
<p>If you have a question, please have a look at the <a href="https://docs.facilmap.org/users/" target="_blank">documentation</a>, raise a question in the <a href="https://github.com/FacilMap/facilmap/discussions" target="_blank">discussion forum</a> or ask in the <a href="https://matrix.to/#/#facilmap:rankenste.in" target="_blank">Matrix chat</a>.</p>
|
||||
<p><a href="https://docs.facilmap.org/users/privacy/" target="_blank">Privacy information</a></p>
|
||||
<h4>Map data</h4>
|
||||
<dl class="row">
|
||||
<template v-for="layer in layers">
|
||||
|
@ -60,17 +60,18 @@
|
|||
<li><a href="https://sequelize.org/" target="_blank">Sequelize</a></li>
|
||||
<li><a href="https://socket.io/" target="_blank">socket.io</a></li>
|
||||
<li><a href="https://www.typescriptlang.org/" target="_blank">TypeScript</a></li>
|
||||
<li><a href="https://webpack.js.org/" target="_blank">Webpack</a></li>
|
||||
<li><a href="https://jquery.com/" target="_blank">jQuery</a></li>
|
||||
<li><a href="https://vuejs.org/" target="_blank">Vue.js</a></li>
|
||||
<li><a href="https://github.com/chjj/marked" target="_blank">Marked</a></li>
|
||||
<li><a href="https://vitejs.dev/" target="_blank">Vite</a></li>
|
||||
<li><a href="https://getbootstrap.com/" target="_blank">Bootstrap</a></li>
|
||||
<li><a href="https://bootstrap-vue.org/" target="_blank">BootstrapVue</a></li>
|
||||
<li><a href="https://leafletjs.com/" target="_blank">Leaflet</a></li>
|
||||
<li><a href="http://project-osrm.org/" target="_blank">OSRM</a></li>
|
||||
<li><a href="https://openrouteservice.org/" target="_blank">OpenRouteService</a></li>
|
||||
<li><a href="https://nominatim.openstreetmap.org/" target="_blank">Nominatim</a></li>
|
||||
<li><a href="https://github.com/joewalnes/filtrex" target="_blank">Filtrex</a></li>
|
||||
<li><a href="https://github.com/chjj/marked" target="_blank">Marked</a></li>
|
||||
<li><a href="https://github.com/cure53/DOMPurify" target="_blank">DOMPurify</a></li>
|
||||
<li><a href="https://expressjs.com/" target="_blank">Express</a></li>
|
||||
<li><a href="https://vuepress.vuejs.org/" target="_blank">Vuepress</a></li>
|
||||
</ul>
|
||||
<h4>Icons</h4>
|
||||
<ul>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { Point, SearchResult } from "facilmap-types";
|
||||
import type { SearchResult } from "facilmap-types";
|
||||
import { round } from "facilmap-utils";
|
||||
import { SearchResultsLayer } from "facilmap-leaflet";
|
||||
import SearchResultInfo from "./search-result-info.vue";
|
||||
import { Util } from "leaflet";
|
||||
import { computed, markRaw, nextTick, ref, shallowReactive, watch } from "vue";
|
||||
import { computed, markRaw, nextTick, readonly, ref, shallowReactive, toRef, watch } from "vue";
|
||||
import { useEventListener } from "../utils/utils";
|
||||
import SearchBoxTab from "./search-box/search-box-tab.vue"
|
||||
import { injectContextRequired, requireClientContext, requireMapContext, requireSearchBoxContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import type { WritableClickMarkerTabContext } from "./facil-map-context-provider/click-marker-tab-context";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const mapContext = requireMapContext(context);
|
||||
|
@ -19,7 +20,6 @@
|
|||
const activeResults = ref<SearchResult[]>([]);
|
||||
const layers = shallowReactive<SearchResultsLayer[]>([]);
|
||||
|
||||
useEventListener(mapContext, "map-long-click", handleMapLongClick);
|
||||
useEventListener(mapContext, "open-selection", handleOpenSelection);
|
||||
|
||||
const layerIds = computed(() => layers.map((layer) => Util.stamp(layer)));
|
||||
|
@ -31,44 +31,48 @@
|
|||
}
|
||||
});
|
||||
|
||||
const clickMarkerTabContext = ref<WritableClickMarkerTabContext>({
|
||||
async openClickMarker(point) {
|
||||
const now = Date.now();
|
||||
lastClick = now;
|
||||
|
||||
const results = await client.value.find({
|
||||
query: `geo:${round(point.lat, 5)},${round(point.lon, 5)}?z=${mapContext.value.zoom}`,
|
||||
loadUrls: false,
|
||||
elevation: true
|
||||
});
|
||||
|
||||
if (now !== lastClick) {
|
||||
// There has been another click since the one we are reacting to.
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
const layer = new SearchResultsLayer([results[0]]).addTo(mapContext.value.components.map);
|
||||
mapContext.value.components.selectionHandler.addSearchResultLayer(layer);
|
||||
|
||||
activeResults.value.push(results[0]);
|
||||
layers.push(markRaw(layer));
|
||||
|
||||
mapContext.value.components.selectionHandler.setSelectedItems([{ type: "searchResult", result: results[0], layerId: Util.stamp(layer) }]);
|
||||
|
||||
await nextTick();
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-click-marker-tab-${activeResults.value.length - 1}`, { expand: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
context.provideComponent("clickMarkerTab", toRef(readonly(clickMarkerTabContext)));
|
||||
|
||||
function handleOpenSelection(): void {
|
||||
for (let i = 0; i < layerIds.value.length; i++) {
|
||||
if (mapContext.value.selection.some((item) => item.type == "searchResult" && item.layerId == layerIds.value[i])) {
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-click-marker-tab-${i}`);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-click-marker-tab-${i}`, { expand: true });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMapLongClick({ point }: { point: Point }): Promise<void> {
|
||||
const now = Date.now();
|
||||
lastClick = now;
|
||||
|
||||
const results = await client.value.find({
|
||||
query: `geo:${round(point.lat, 5)},${round(point.lon, 5)}?z=${mapContext.value.zoom}`,
|
||||
loadUrls: false,
|
||||
elevation: true
|
||||
});
|
||||
|
||||
if (now !== lastClick) {
|
||||
// There has been another click since the one we are reacting to.
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
const layer = new SearchResultsLayer([results[0]]).addTo(mapContext.value.components.map);
|
||||
mapContext.value.components.selectionHandler.addSearchResultLayer(layer);
|
||||
|
||||
activeResults.value.push(results[0]);
|
||||
layers.push(markRaw(layer));
|
||||
|
||||
mapContext.value.components.selectionHandler.setSelectedItems([{ type: "searchResult", result: results[0], layerId: Util.stamp(layer) }]);
|
||||
|
||||
await nextTick();
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-click-marker-tab-${activeResults.value.length - 1}`);
|
||||
}
|
||||
}
|
||||
|
||||
function close(result: SearchResult): void {
|
||||
const idx = activeResults.value.indexOf(result);
|
||||
if (idx == -1)
|
||||
|
|
|
@ -52,9 +52,9 @@
|
|||
toasts.hideToast(`fm${context.id}-client-deleted`);
|
||||
createId.value = undefined;
|
||||
if (props.padId)
|
||||
toasts.showToast(`fm${context.id}-client-connecting`, "Loading", "Loading map…", { spinner: true });
|
||||
toasts.showToast(`fm${context.id}-client-connecting`, "Loading", "Loading map…", { spinner: true, noCloseButton: true });
|
||||
else
|
||||
toasts.showToast(`fm${context.id}-client-connecting`, "Connecting", "Connecting to server…", { spinner: true });
|
||||
toasts.showToast(`fm${context.id}-client-connecting`, "Connecting", "Connecting to server…", { spinner: true, noCloseButton: true });
|
||||
|
||||
const newClient: ClientContext = Object.assign(new ReactiveClient(props.serverUrl, props.padId), {
|
||||
openPad
|
||||
|
@ -82,6 +82,7 @@
|
|||
|
||||
newClient.on("deletePad", () => {
|
||||
toasts.showToast(`fm${context.id}-client-deleted`, "Map deleted", "This map has been deleted.", {
|
||||
noCloseButton: true,
|
||||
variant: "danger",
|
||||
actions: context.settings.interactive ? [
|
||||
{
|
||||
|
|
|
@ -38,13 +38,13 @@
|
|||
class="fm-edit-filter"
|
||||
:isModified="isModified"
|
||||
@submit="save"
|
||||
okLabel="Apply"
|
||||
:okLabel="isModified ? 'Apply' : undefined"
|
||||
ref="modalRef"
|
||||
@hidden="emit('hidden')"
|
||||
>
|
||||
<p>Here you can set an advanced expression to show/hide certain markers/lines based on their attributes. The filter expression only applies to your view of the map, but it can be persisted as part of a saved view or a shared link.</p>
|
||||
|
||||
<div class="was-validated">
|
||||
<div :class="{ 'was-validated': filter }">
|
||||
<textarea
|
||||
class="form-control text-monospace"
|
||||
v-model="filter"
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
import { canControl, getUniqueId, mergeObject, validateRequired } from "../utils/utils";
|
||||
import { cloneDeep, isEqual, omit } from "lodash-es";
|
||||
import ModalDialog from "./ui/modal-dialog.vue";
|
||||
import ColourField from "./ui/colour-field.vue";
|
||||
import ColourPicker from "./ui/colour-picker.vue";
|
||||
import FieldInput from "./ui/field-input.vue";
|
||||
import RouteMode from "./ui/route-mode.vue";
|
||||
import WidthField from "./ui/width-field.vue";
|
||||
import WidthPicker from "./ui/width-picker.vue";
|
||||
import { computed, ref, toRef, watch } from "vue";
|
||||
import { useToasts } from "./ui/toasts/toasts.vue";
|
||||
import DropdownMenu from "./ui/dropdown-menu.vue";
|
||||
|
@ -89,11 +89,11 @@
|
|||
<div class="row mb-3">
|
||||
<label :for="`${id}-colour-input`" class="col-sm-3 col-form-label">Colour</label>
|
||||
<div class="col-sm-9">
|
||||
<ColourField
|
||||
<ColourPicker
|
||||
:id="`${id}-colour-input`"
|
||||
v-model="line.colour"
|
||||
:validationError="colourValidationError"
|
||||
></ColourField>
|
||||
></ColourPicker>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -102,11 +102,11 @@
|
|||
<div class="row mb-3">
|
||||
<label :for="`${id}-width-input`" class="col-sm-3 col-form-label">Width</label>
|
||||
<div class="col-sm-9">
|
||||
<WidthField
|
||||
<WidthPicker
|
||||
:id="`${id}-width-input`"
|
||||
v-model="line.width"
|
||||
class="fm-form-range-with-label"
|
||||
></WidthField>
|
||||
></WidthPicker>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
import { canControl, getUniqueId, mergeObject, validateRequired } from "../utils/utils";
|
||||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import ModalDialog from "./ui/modal-dialog.vue";
|
||||
import ColourField from "./ui/colour-field.vue";
|
||||
import SymbolField from "./ui/symbol-field.vue";
|
||||
import ShapeField from "./ui/shape-field.vue";
|
||||
import ColourPicker from "./ui/colour-picker.vue";
|
||||
import SymbolPicker from "./ui/symbol-picker.vue";
|
||||
import ShapePicker from "./ui/shape-picker.vue";
|
||||
import FieldInput from "./ui/field-input.vue";
|
||||
import SizeField from "./ui/size-field.vue";
|
||||
import SizePicker from "./ui/size-picker.vue";
|
||||
import { computed, ref, toRef, watch } from "vue";
|
||||
import { useToasts } from "./ui/toasts/toasts.vue";
|
||||
import DropdownMenu from "./ui/dropdown-menu.vue";
|
||||
|
@ -82,11 +82,11 @@
|
|||
<div class="row mb-3">
|
||||
<label :for="`${id}-colour-input`" class="col-sm-3 col-form-label">Colour</label>
|
||||
<div class="col-sm-9">
|
||||
<ColourField
|
||||
<ColourPicker
|
||||
:id="`${id}-colour-input`"
|
||||
v-model="marker.colour"
|
||||
:validationError="colourValidationError"
|
||||
></ColourField>
|
||||
></ColourPicker>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -95,11 +95,11 @@
|
|||
<div class="row mb-3">
|
||||
<label :for="`${id}-size-input`" class="col-sm-3 col-form-label">Size</label>
|
||||
<div class="col-sm-9">
|
||||
<SizeField
|
||||
<SizePicker
|
||||
:id="`${id}-size-input`"
|
||||
v-model="marker.size"
|
||||
class="fm-form-range-with-label"
|
||||
></SizeField>
|
||||
></SizePicker>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -108,7 +108,7 @@
|
|||
<div class="row mb-3">
|
||||
<label :for="`${id}-symbol-input`" class="col-sm-3 col-form-label">Icon</label>
|
||||
<div class="col-sm-9">
|
||||
<SymbolField :id="`${id}-symbol-input`" v-model="marker.symbol"></SymbolField>
|
||||
<SymbolPicker :id="`${id}-symbol-input`" v-model="marker.symbol"></SymbolPicker>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<div class="row mb-3">
|
||||
<label :for="`${id}-shape-input`" class="col-sm-3 col-form-label">Shape</label>
|
||||
<div class="col-sm-9">
|
||||
<ShapeField :id="`${id}-shape-input`" v-model="marker.shape"></ShapeField>
|
||||
<ShapePicker :id="`${id}-shape-input`" v-model="marker.shape"></ShapePicker>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -4,15 +4,15 @@
|
|||
import { mergeTypeObject } from "./edit-type-utils";
|
||||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import { useToasts } from "../ui/toasts/toasts.vue";
|
||||
import ColourField from "../ui/colour-field.vue";
|
||||
import ShapeField from "../ui/shape-field.vue";
|
||||
import SymbolField from "../ui/symbol-field.vue";
|
||||
import ColourPicker from "../ui/colour-picker.vue";
|
||||
import ShapePicker from "../ui/shape-picker.vue";
|
||||
import SymbolPicker from "../ui/symbol-picker.vue";
|
||||
import RouteMode from "../ui/route-mode.vue";
|
||||
import Draggable from "vuedraggable";
|
||||
import FieldInput from "../ui/field-input.vue";
|
||||
import Icon from "../ui/icon.vue";
|
||||
import WidthField from "../ui/width-field.vue";
|
||||
import SizeField from "../ui/size-field.vue";
|
||||
import WidthPicker from "../ui/width-picker.vue";
|
||||
import SizePicker from "../ui/size-picker.vue";
|
||||
import EditTypeDropdownDialog from "./edit-type-dropdown-dialog.vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import ModalDialog from "../ui/modal-dialog.vue";
|
||||
|
@ -77,7 +77,12 @@
|
|||
}
|
||||
|
||||
async function deleteField(field: Field): Promise<void> {
|
||||
if (!await showConfirm({ title: "Delete field", message: `Do you really want to delete the field “${field.name}”?` }))
|
||||
if (!await showConfirm({
|
||||
title: "Delete field",
|
||||
message: `Do you really want to delete the field “${field.name}”?`,
|
||||
variant: "danger",
|
||||
okLabel: "Delete"
|
||||
}))
|
||||
return;
|
||||
|
||||
var idx = type.value.fields.indexOf(field);
|
||||
|
@ -178,6 +183,7 @@
|
|||
class="fm-edit-type"
|
||||
:isModified="isModified"
|
||||
:isCreate="isCreate"
|
||||
:stackLevel="1"
|
||||
@submit="$event.waitUntil(save())"
|
||||
@hidden="emit('hidden')"
|
||||
>
|
||||
|
@ -225,11 +231,11 @@
|
|||
<div class="col-sm-9">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-sm-9">
|
||||
<ColourField
|
||||
<ColourPicker
|
||||
:id="`${id}-default-colour-input`"
|
||||
v-model="type.defaultColour"
|
||||
:validationError="defaultColourValidationError"
|
||||
></ColourField>
|
||||
></ColourPicker>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-check">
|
||||
|
@ -253,11 +259,11 @@
|
|||
<div class="col-sm-9">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-sm-9">
|
||||
<SizeField
|
||||
<SizePicker
|
||||
:id="`${id}-default-size-input`"
|
||||
v-model="type.defaultSize"
|
||||
:validationError="defaultSizeValidationError"
|
||||
></SizeField>
|
||||
></SizePicker>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-check">
|
||||
|
@ -281,11 +287,11 @@
|
|||
<div class="col-sm-9">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-sm-9">
|
||||
<SymbolField
|
||||
<SymbolPicker
|
||||
:id="`${id}-default-symbol-input`"
|
||||
v-model="type.defaultSymbol"
|
||||
:validationError="defaultSymbolValidationError"
|
||||
></SymbolField>
|
||||
></SymbolPicker>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-check">
|
||||
|
@ -309,11 +315,11 @@
|
|||
<div class="col-sm-9">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-sm-9">
|
||||
<ShapeField
|
||||
<ShapePicker
|
||||
:id="`${id}-default-shape-input`"
|
||||
v-model="type.defaultShape"
|
||||
:validationError="defaultShapeValidationError"
|
||||
></ShapeField>
|
||||
></ShapePicker>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-check">
|
||||
|
@ -337,11 +343,11 @@
|
|||
<div class="col-sm-9">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-sm-9">
|
||||
<WidthField
|
||||
<WidthPicker
|
||||
:id="`${id}-default-width-input`"
|
||||
v-model="type.defaultWidth"
|
||||
:validationError="defaultWidthValidationError"
|
||||
></WidthField>
|
||||
></WidthPicker>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-check">
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
import type { Field, FieldOption, FieldOptionUpdate, FieldUpdate, Type } from "facilmap-types";
|
||||
import { canControl, getUniqueId, mergeObject, validateRequired } from "../../utils/utils";
|
||||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import ColourField from "../ui/colour-field.vue";
|
||||
import ColourPicker from "../ui/colour-picker.vue";
|
||||
import Draggable from "vuedraggable";
|
||||
import Icon from "../ui/icon.vue";
|
||||
import ModalDialog from "../ui/modal-dialog.vue";
|
||||
import ShapeField from "../ui/shape-field.vue";
|
||||
import SizeField from "../ui/size-field.vue";
|
||||
import SymbolField from "../ui/symbol-field.vue";
|
||||
import WidthField from "../ui/width-field.vue";
|
||||
import ShapePicker from "../ui/shape-picker.vue";
|
||||
import SizePicker from "../ui/size-picker.vue";
|
||||
import SymbolPicker from "../ui/symbol-picker.vue";
|
||||
import WidthPicker from "../ui/width-picker.vue";
|
||||
import { useToasts } from "../ui/toasts/toasts.vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { showConfirm } from "../ui/alert.vue";
|
||||
|
@ -96,7 +96,12 @@
|
|||
}
|
||||
|
||||
async function deleteOption(option: FieldOptionUpdate): Promise<void> {
|
||||
if (!await showConfirm({ title: "Delete option", message: `Do you really want to delete the option “${option.value}”?` }))
|
||||
if (!await showConfirm({
|
||||
title: "Delete option",
|
||||
message: `Do you really want to delete the option “${option.value}”?`,
|
||||
variant: "danger",
|
||||
okLabel: "Delete"
|
||||
}))
|
||||
return;
|
||||
|
||||
var idx = fieldValue.value.options!.indexOf(option);
|
||||
|
@ -144,7 +149,8 @@
|
|||
@submit="save()"
|
||||
@hidden="emit('hidden')"
|
||||
:size="fieldValue && controlNumber > 2 ? 'xl' : 'lg'"
|
||||
okLabel="OK"
|
||||
:okLabel="isModified ? 'OK' : undefined"
|
||||
:stackLevel="2"
|
||||
>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label">Control</label>
|
||||
|
@ -262,19 +268,19 @@
|
|||
</div>
|
||||
</td>
|
||||
<td v-if="fieldValue.controlColour" class="field">
|
||||
<ColourField v-model="option.colour" :validationError="optionValidationErrors![idx].colour"></ColourField>
|
||||
<ColourPicker v-model="option.colour" :validationError="optionValidationErrors![idx].colour"></ColourPicker>
|
||||
</td>
|
||||
<td v-if="fieldValue.controlSize" class="field">
|
||||
<SizeField v-model="option.size" :validationError="optionValidationErrors![idx].colour"></SizeField>
|
||||
<SizePicker v-model="option.size" :validationError="optionValidationErrors![idx].colour"></SizePicker>
|
||||
</td>
|
||||
<td v-if="fieldValue.controlSymbol" class="field">
|
||||
<SymbolField v-model="option.symbol"></SymbolField>
|
||||
<SymbolPicker v-model="option.symbol"></SymbolPicker>
|
||||
</td>
|
||||
<td v-if="fieldValue.controlShape" class="field">
|
||||
<ShapeField v-model="option.shape"></ShapeField>
|
||||
<ShapePicker v-model="option.shape"></ShapePicker>
|
||||
</td>
|
||||
<td v-if="fieldValue.controlWidth" class="field">
|
||||
<WidthField v-model="option.width" :validationError="optionValidationErrors![idx].width"></WidthField>
|
||||
<WidthPicker v-model="option.width" :validationError="optionValidationErrors![idx].width"></WidthPicker>
|
||||
</td>
|
||||
<td v-if="fieldValue.type != 'checkbox'" class="td-buttons">
|
||||
<button type="button" class="btn btn-secondary" @click="deleteOption(option)"><Icon icon="minus" alt="Remove"></Icon></button>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import type { Point } from "facilmap-types";
|
||||
|
||||
export interface WritableClickMarkerTabContext {
|
||||
openClickMarker(point: Point): Promise<void>;
|
||||
}
|
||||
|
||||
export type ClickMarkerTabContext = Readonly<WritableClickMarkerTabContext>;
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
const components = shallowReactive<FacilMapComponents>({});
|
||||
|
||||
function provideComponent<K extends keyof FacilMapComponents>(key: K, componentRef: Ref<FacilMapComponents[K]>) {
|
||||
function provideComponent<K extends keyof FacilMapComponents>(key: K, componentRef: Readonly<Ref<FacilMapComponents[K]>>) {
|
||||
if (key in components) {
|
||||
throw new Error(`Component "${key}"" is already provided.`);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,10 @@ import type { Ref } from "vue";
|
|||
import type { ClientContext } from "./client-context";
|
||||
import type { MapContext } from "./map-context";
|
||||
import type { SearchBoxContext } from "./search-box-context";
|
||||
import type { SearchFormTabContext } from "./search-form-tab-context";
|
||||
import type { RouteFormTabContext } from "./route-form-tab-context";
|
||||
import type { ClickMarkerTabContext } from "./click-marker-tab-context";
|
||||
import type { ImportTabContext } from "./import-tab-context";
|
||||
|
||||
export interface FacilMapSettings {
|
||||
toolbox: boolean;
|
||||
|
@ -17,6 +21,10 @@ export interface FacilMapComponents {
|
|||
searchBox?: SearchBoxContext;
|
||||
client?: ClientContext;
|
||||
map?: MapContext;
|
||||
searchFormTab?: SearchFormTabContext;
|
||||
routeFormTab?: RouteFormTabContext;
|
||||
clickMarkerTab?: ClickMarkerTabContext;
|
||||
importTab?: ImportTabContext;
|
||||
}
|
||||
|
||||
export interface WritableFacilMapContext {
|
||||
|
@ -25,7 +33,7 @@ export interface WritableFacilMapContext {
|
|||
isNarrow: boolean;
|
||||
settings: FacilMapSettings;
|
||||
components: FacilMapComponents;
|
||||
provideComponent<K extends keyof FacilMapComponents>(key: K, componentRef: Ref<FacilMapComponents[K]>): void;
|
||||
provideComponent<K extends keyof FacilMapComponents>(key: K, componentRef: Readonly<Ref<FacilMapComponents[K]>>): void;
|
||||
}
|
||||
|
||||
export type FacilMapContext = Readonly<Omit<WritableFacilMapContext, "settings" | "components">> & {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface WritableImportTabContext {
|
||||
openFilePicker: () => void;
|
||||
}
|
||||
|
||||
export type ImportTabContext = Readonly<WritableImportTabContext>;
|
|
@ -1,4 +1,3 @@
|
|||
import type { FindOnMapResult, Point, SearchResult } from "facilmap-types";
|
||||
import type { BboxHandler, HashHandler, HashQuery, LinesLayer, MarkersLayer, OverpassLayer, OverpassPreset, SearchResultsLayer, VisibleLayers } from "facilmap-leaflet";
|
||||
import type { LatLng, LatLngBounds, Map } from "leaflet";
|
||||
import type { FilterFunc } from "facilmap-utils";
|
||||
|
@ -7,22 +6,8 @@ import type { DeepReadonly } from "vue";
|
|||
import type { SelectedItem } from "../../utils/selection";
|
||||
import type SelectionHandler from "../../utils/selection";
|
||||
|
||||
export interface RouteDestination {
|
||||
query: string;
|
||||
searchSuggestions?: SearchResult[];
|
||||
mapSuggestions?: FindOnMapResult[];
|
||||
selectedSuggestion?: SearchResult | FindOnMapResult;
|
||||
}
|
||||
|
||||
export type MapContextEvents = {
|
||||
"import-file": void;
|
||||
"open-selection": { selection: DeepReadonly<SelectedItem[]> };
|
||||
"search-set-query": { query: string; zoom?: boolean; smooth?: boolean };
|
||||
"route-set-query": { query: string; zoom?: boolean; smooth?: boolean };
|
||||
"route-set-from": RouteDestination;
|
||||
"route-add-via": RouteDestination;
|
||||
"route-set-to": RouteDestination;
|
||||
"map-long-click": { point: Point };
|
||||
};
|
||||
|
||||
export interface MapComponents {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import type { FindOnMapResult, SearchResult } from "facilmap-types";
|
||||
|
||||
export interface RouteDestination {
|
||||
query: string;
|
||||
searchSuggestions?: SearchResult[];
|
||||
mapSuggestions?: FindOnMapResult[];
|
||||
selectedSuggestion?: SearchResult | FindOnMapResult;
|
||||
}
|
||||
|
||||
export interface WritableRouteFormTabContext {
|
||||
setQuery(query: string, zoom?: boolean, smooth?: boolean): void;
|
||||
setFrom(destination: RouteDestination): void;
|
||||
addVia(destination: RouteDestination): void;
|
||||
setTo(destination: RouteDestination): void;
|
||||
}
|
||||
|
||||
export type RouteFormTabContext = Readonly<WritableRouteFormTabContext>;
|
|
@ -7,7 +7,6 @@ export type SearchBoxEventMap = {
|
|||
"resize": void;
|
||||
"resizeend": void;
|
||||
"resizereset": void;
|
||||
"expand": void;
|
||||
}
|
||||
|
||||
export interface SearchBoxTab {
|
||||
|
@ -23,7 +22,7 @@ export interface SearchBoxContextData {
|
|||
activeTabId: string | undefined;
|
||||
activeTab: SearchBoxTab | undefined;
|
||||
provideTab: (id: string, tabRef: Ref<SearchBoxTab>) => void;
|
||||
activateTab: (id: string, expand?: boolean) => void;
|
||||
activateTab: (id: string, options?: { expand?: boolean; autofocus?: boolean }) => void;
|
||||
}
|
||||
|
||||
export type WritableSearchBoxContext = SearchBoxContextData & Emitter<SearchBoxEventMap>;
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface WritableSearchFormTabContext {
|
||||
setQuery(query: string, zoom?: boolean, smooth?: boolean, autofocus?: boolean): void;
|
||||
}
|
||||
|
||||
export type SearchFormTabContext = Readonly<WritableSearchFormTabContext>;
|
|
@ -140,7 +140,6 @@
|
|||
</ul>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
</SearchResults>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import type { HistoryEntry, ID } from "facilmap-types";
|
||||
import { orderBy } from "lodash-es";
|
||||
import Icon from "../ui/icon.vue";
|
||||
import { computed, onScopeDispose, reactive, ref } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import { useToasts } from "../ui/toasts/toasts.vue";
|
||||
import { showConfirm } from "../ui/alert.vue";
|
||||
import { mapRef } from "../../utils/vue";
|
||||
|
@ -33,15 +33,17 @@
|
|||
|
||||
const id = getUniqueId("fm-history-dialog");
|
||||
|
||||
try {
|
||||
await client.value.listenToHistory();
|
||||
} catch (err) {
|
||||
toasts.showErrorToast(`${id}-listen-error`, "Error loading history", err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await client.value.listenToHistory();
|
||||
} catch (err) {
|
||||
toasts.showErrorToast(`${id}-listen-error`, "Error loading history", err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
onScopeDispose(async () => {
|
||||
onBeforeUnmount(async () => {
|
||||
try {
|
||||
await client.value.stopListeningToHistory();
|
||||
} catch (err) {
|
||||
|
@ -52,7 +54,12 @@
|
|||
async function revert(entry: HistoryEntryWithLabels): Promise<void> {
|
||||
toasts.hideToast(`${id}-revert-error`);
|
||||
|
||||
if (!await showConfirm({ title: entry.labels.button, message: entry.labels.confirm }))
|
||||
if (!await showConfirm({
|
||||
title: entry.labels.revert!.button,
|
||||
message: entry.labels.revert!.message,
|
||||
variant: "warning",
|
||||
okLabel: entry.labels.revert!.okLabel
|
||||
}))
|
||||
return;
|
||||
|
||||
isReverting.value = entry;
|
||||
|
@ -82,7 +89,7 @@
|
|||
const isShown = activeDiffPopoverId.value === entryId;
|
||||
const show = force ?? (activeDiffPopoverId.value !== entryId);
|
||||
if (isShown !== show) {
|
||||
activeDiffPopoverId.value = show ? activeDiffPopoverId.value : undefined;
|
||||
activeDiffPopoverId.value = show ? entryId : undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -99,7 +106,7 @@
|
|||
<div v-if="isLoading" class="d-flex justify-content-center">
|
||||
<div class="spinner-border"></div>
|
||||
</div>
|
||||
<table v-else class="table table-striped table-hover">
|
||||
<table v-else class="table table-striped table-hover history-entries">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="min-width: 12rem">Date</th>
|
||||
|
@ -152,14 +159,14 @@
|
|||
<td class="td-buttons">
|
||||
<div class="d-grid">
|
||||
<button
|
||||
v-if="entry.labels.button"
|
||||
v-if="entry.labels.revert"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:disabled="!!isReverting"
|
||||
@click="revert(entry)"
|
||||
>
|
||||
<div v-if="isReverting === entry" class="spinner-border spinner-border-sm"></div>
|
||||
{{entry.labels.button}}
|
||||
{{entry.labels.revert.button}}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -178,4 +185,11 @@
|
|||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-history {
|
||||
.history-entries > tbody > tr {
|
||||
// Make sure that lines without button have the same height as lines with button
|
||||
height: calc(/* button line-height */ 1.5rem + /* button padding */ 2 * 0.375rem + /* button border */ 2 * 1px + /* td padding */ 2 * 0.5rem + /* td border-bottom */ 1px);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -22,52 +22,62 @@ function existsNow(client: ClientContext, entry: HistoryEntry) {
|
|||
|
||||
export interface HistoryEntryLabels {
|
||||
description: string;
|
||||
button: string;
|
||||
confirm: string;
|
||||
revert?: {
|
||||
title: string;
|
||||
message: string;
|
||||
button: string;
|
||||
okLabel: string;
|
||||
},
|
||||
diff?: Array<ObjectDiffItem>;
|
||||
}
|
||||
|
||||
export function getLabelsForHistoryEntry(client: ClientContext, entry: HistoryEntry): HistoryEntryLabels {
|
||||
if(entry.type == "Pad") {
|
||||
return {
|
||||
description: "Changed pad settings",
|
||||
button: "Revert",
|
||||
confirm: "Do you really want to restore the old version of the pad settings?",
|
||||
...(entry.objectBefore && entry.objectAfter ? { diff: getObjectDiff(entry.objectBefore, entry.objectAfter) } : {})
|
||||
description: "Changed map settings",
|
||||
revert: {
|
||||
title: "Revert map settings",
|
||||
message: "Do you really want to restore the old version of the map settings?",
|
||||
button: "Revert",
|
||||
okLabel: "Revert"
|
||||
},
|
||||
...(entry.objectBefore && entry.objectAfter ? { diff: getObjectDiff(entry.objectBefore, entry.objectAfter) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const nameStrBefore = entry.objectBefore && entry.objectBefore.name ? "“" + entry.objectBefore.name + "”" : "";
|
||||
const nameStrAfter = entry.objectAfter && entry.objectAfter.name ? "“" + entry.objectAfter.name + "”" : "";
|
||||
|
||||
const nameStrBefore = entry.objectBefore && entry.objectBefore.name ? `“${entry.objectBefore.name}”` : "";
|
||||
const nameStrAfter = entry.objectAfter && entry.objectAfter.name ? `“${entry.objectAfter.name}”` : "";
|
||||
const exists = existsNow(client, entry);
|
||||
|
||||
const ret: Partial<HistoryEntryLabels> = {
|
||||
description: {
|
||||
create: "Created",
|
||||
update: "Changed",
|
||||
delete: "Deleted"
|
||||
}[entry.action] + " " + entry.type + " " + entry.objectId + " " + (nameStrBefore && nameStrAfter && nameStrBefore != nameStrAfter ? nameStrBefore + " (new name: " + nameStrAfter + ")" : (nameStrBefore || nameStrAfter)),
|
||||
};
|
||||
|
||||
if(entry.action == "create") {
|
||||
if(exists) {
|
||||
ret.button = "Revert (delete)";
|
||||
ret.confirm = "delete";
|
||||
const descriptionAction = { create: "Created", update: "Changed", delete: "Deleted" }[entry.action];
|
||||
const descriptionName = (nameStrBefore && nameStrAfter && nameStrBefore != nameStrAfter ? `${nameStrBefore} (new name: ${nameStrAfter})` : (nameStrBefore || nameStrAfter));
|
||||
const diff = entry.objectBefore && entry.objectAfter ? getObjectDiff(entry.objectBefore, entry.objectAfter) : undefined;
|
||||
const revert = (
|
||||
entry.action === "create" ? (
|
||||
exists ? {
|
||||
title: `Delete ${entry.type}`,
|
||||
button: "Revert (delete)",
|
||||
message: "delete",
|
||||
okLabel: "Delete"
|
||||
} : undefined
|
||||
) : exists ? {
|
||||
title: `Revert ${entry.type}`,
|
||||
button: "Revert",
|
||||
message: "restore the old version of",
|
||||
okLabel: "Revert"
|
||||
} : {
|
||||
title: `Restore ${entry.type}`,
|
||||
button: "Restore",
|
||||
message: "restore",
|
||||
okLabel: "Restore"
|
||||
}
|
||||
} else if(exists) {
|
||||
ret.button = "Revert";
|
||||
ret.confirm = "restore the old version of";
|
||||
} else {
|
||||
ret.button = "Restore";
|
||||
ret.confirm = "restore";
|
||||
}
|
||||
);
|
||||
|
||||
if(ret.confirm)
|
||||
ret.confirm = "Do you really want to " + ret.confirm + " " + ((nameStrBefore || nameStrAfter) ? "the " + entry.type + " " + (nameStrBefore || nameStrAfter) : "this " + entry.type);
|
||||
|
||||
if(entry.objectBefore && entry.objectAfter)
|
||||
ret.diff = getObjectDiff(entry.objectBefore, entry.objectAfter);
|
||||
|
||||
return ret as HistoryEntryLabels;
|
||||
return {
|
||||
description: `${descriptionAction} ${entry.type} ${entry.objectId} ${descriptionName}`,
|
||||
revert: revert && {
|
||||
...revert,
|
||||
message: `Do you really want to ${revert.message} ${((nameStrBefore || nameStrAfter) ? `the ${entry.type} ${(nameStrBefore || nameStrAfter)}` : `this ${entry.type}`)}?`
|
||||
},
|
||||
...(diff ? { diff } : {})
|
||||
};
|
||||
}
|
|
@ -5,10 +5,11 @@
|
|||
import { Util } from "leaflet";
|
||||
import FileResults from "./file-results.vue";
|
||||
import SearchBoxTab from "./search-box/search-box-tab.vue";
|
||||
import { computed, markRaw, ref, shallowReactive } from "vue";
|
||||
import { computed, markRaw, readonly, ref, shallowReactive, toRef } from "vue";
|
||||
import { useDomEventListener, useEventListener } from "../utils/utils";
|
||||
import { useToasts } from "./ui/toasts/toasts.vue";
|
||||
import { injectContextRequired, requireMapContext, requireSearchBoxContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import type { WritableImportTabContext } from "./facil-map-context-provider/import-tab-context";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const mapContext = requireMapContext(context);
|
||||
|
@ -20,7 +21,6 @@
|
|||
const files = ref<Array<FileResultObject & { title: string }>>([]);
|
||||
const layers = shallowReactive<SearchResultsLayer[]>([]);
|
||||
|
||||
useEventListener(mapContext, "import-file", handleImportFile);
|
||||
useEventListener(mapContext, "open-selection", handleOpenSelection);
|
||||
|
||||
useDomEventListener(mapContext.value.components.container, "dragenter", handleMapDragEnter);
|
||||
|
@ -29,14 +29,18 @@
|
|||
|
||||
const layerIds = computed(() => layers.map((layer) => Util.stamp(layer)));
|
||||
|
||||
function handleImportFile(): void {
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
const importTabContext = ref<WritableImportTabContext>({
|
||||
openFilePicker() {
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
});
|
||||
|
||||
context.provideComponent("importTab", toRef(readonly(importTabContext)));
|
||||
|
||||
function handleOpenSelection(): void {
|
||||
for (let i = 0; i < layerIds.value.length; i++) {
|
||||
if (mapContext.value.selection.some((item) => item.type == "searchResult" && item.layerId == layerIds.value[i])) {
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-import-tab-${i}`);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-import-tab-${i}`, { expand: true });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +97,7 @@
|
|||
files.value.push(result);
|
||||
layers.push(layer);
|
||||
setTimeout(() => {
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-import-tab-${files.value.length - 1}`);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-import-tab-${files.value.length - 1}`, { expand: true });
|
||||
}, 0);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import type { MapComponents, MapContextData, MapContextEvents, WritableMapContex
|
|||
import type { ClientContext } from "../facil-map-context-provider/client-context";
|
||||
import type { FacilMapContext } from "../facil-map-context-provider/facil-map-context";
|
||||
import { requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import { sleep } from "facilmap-utils";
|
||||
|
||||
type MapContextWithoutComponents = Optional<WritableMapContext, 'components'>;
|
||||
|
||||
|
@ -128,7 +129,7 @@ function createSearchResultsLayer(map: Map): SearchResultsLayer {
|
|||
return new SearchResultsLayer(undefined, { pathOptions: { weight: 7 } }).addTo(map);
|
||||
}
|
||||
|
||||
function createSelectionHandler(map: Map, mapContext: MapContextWithoutComponents, markersLayer: MarkersLayer, linesLayer: LinesLayer, searchResultsLayer: SearchResultsLayer, overpassLayer: OverpassLayer): SelectionHandler {
|
||||
function createSelectionHandler(map: Map, context: FacilMapContext, mapContext: MapContextWithoutComponents, markersLayer: MarkersLayer, linesLayer: LinesLayer, searchResultsLayer: SearchResultsLayer, overpassLayer: OverpassLayer): SelectionHandler {
|
||||
const selectionHandler = new SelectionHandler(map, markersLayer, linesLayer, searchResultsLayer, overpassLayer).enable()
|
||||
|
||||
selectionHandler.on("fmChangeSelection", (event: any) => {
|
||||
|
@ -143,7 +144,7 @@ function createSelectionHandler(map: Map, mapContext: MapContextWithoutComponent
|
|||
});
|
||||
|
||||
selectionHandler.on("fmLongClick", (event: any) => {
|
||||
mapContext.emit("map-long-click", { point: { lat: event.latlng.lat, lon: event.latlng.lng } });
|
||||
context.components.clickMarkerTab?.openClickMarker({ lat: event.latlng.lat, lon: event.latlng.lng });
|
||||
});
|
||||
|
||||
onScopeDispose(() => {
|
||||
|
@ -157,16 +158,19 @@ function createHashHandler(map: Map, client: ClientContext, context: FacilMapCon
|
|||
const hashHandler = new HashHandler(map, client, { overpassLayer, simulate: !context.settings.updateHash })
|
||||
.on("fmQueryChange", async (e: any) => {
|
||||
let smooth = true;
|
||||
let autofocus = false;
|
||||
if (!mapContext.components) {
|
||||
// This is called while the hash handler is being enabled, so it is the initial view
|
||||
smooth = false;
|
||||
await nextTick();
|
||||
autofocus = context.settings.autofocus;
|
||||
await sleep(0); // Wait for components to be initialized (needed by openSpecialQuery())
|
||||
}
|
||||
|
||||
const searchFormTab = context.components.searchFormTab;
|
||||
if (!e.query)
|
||||
mapContext.emit("search-set-query", { query: "", zoom: false, smooth: false });
|
||||
searchFormTab?.setQuery("", false, false);
|
||||
else if (!await openSpecialQuery(e.query, context, e.zoom, smooth))
|
||||
mapContext.emit("search-set-query", { query: e.query, zoom: e.zoom, smooth });
|
||||
searchFormTab?.setQuery(e.query, e.zoom, smooth, autofocus);
|
||||
})
|
||||
.enable();
|
||||
|
||||
|
@ -193,7 +197,7 @@ function createMapComponents(context: FacilMapContext, mapContext: MapContextWit
|
|||
const mousePosition = createMousePosition(map);
|
||||
const overpassLayer = createOverpassLayer(map, mapContext);
|
||||
const searchResultsLayer = createSearchResultsLayer(map);
|
||||
const selectionHandler = createSelectionHandler(map, mapContext, markersLayer, linesLayer, searchResultsLayer, overpassLayer);
|
||||
const selectionHandler = createSelectionHandler(map, context, mapContext, markersLayer, linesLayer, searchResultsLayer, overpassLayer);
|
||||
const hashHandler = createHashHandler(map, client, context, mapContext, overpassLayer);
|
||||
|
||||
const mapComponents: MapComponents = {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import "leaflet-mouse-position/src/L.Control.MousePosition.css";
|
||||
import { createMapContext } from "./leaflet-map-components";
|
||||
import vTooltip from "../../utils/tooltip";
|
||||
import type { MapContext } from "../facil-map-context-provider/map-context";
|
||||
import type { WritableMapContext } from "../facil-map-context-provider/map-context";
|
||||
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
|
||||
const context = injectContextRequired();
|
||||
|
@ -25,7 +25,7 @@
|
|||
return `${location.origin}${location.pathname}${mapContext.value?.hash ? `#${mapContext.value.hash}` : ''}`;
|
||||
});
|
||||
|
||||
const mapContext = ref<MapContext>();
|
||||
const mapContext = ref<WritableMapContext>();
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
|
|
|
@ -39,7 +39,7 @@ import { normalizeLineName } from "facilmap-utils";
|
|||
|
||||
function handleOpenSelection(): void {
|
||||
if (line.value)
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-line-info-tab`)
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-line-info-tab`, { expand: true })
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
|
|
|
@ -46,7 +46,12 @@
|
|||
async function deleteLine(): Promise<void> {
|
||||
toasts.hideToast(`fm${context.id}-line-info-delete`);
|
||||
|
||||
if (!await showConfirm({ title: "Remove line", message: `Do you really want to remove the line “${normalizeLineName(line.value.name)}”?` }))
|
||||
if (!await showConfirm({
|
||||
title: "Delete line",
|
||||
message: `Do you really want to delete the line “${normalizeLineName(line.value.name)}”?`,
|
||||
variant: "danger",
|
||||
okLabel: "Delete"
|
||||
}))
|
||||
return;
|
||||
|
||||
isDeleting.value = true;
|
||||
|
@ -85,19 +90,6 @@
|
|||
|
||||
mapContext.value.components.linesLayer.hideLine(line.value.id);
|
||||
|
||||
toasts.showToast(`fm${context.id}-line-info-move`, `Edit waypoints`, "Use the routing form or drag the line around to change it. Click “Finish” to save the changes.", {
|
||||
actions: [
|
||||
{ label: "Finish", onClick: () => { done(true); }},
|
||||
{ label: "Cancel", onClick: () => { done(false); } }
|
||||
]
|
||||
});
|
||||
|
||||
isMoving.value = true;
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve);
|
||||
});
|
||||
|
||||
const done = async (save: boolean) => {
|
||||
const route = client.value.routes[routeId];
|
||||
if (save && !route)
|
||||
|
@ -122,6 +114,16 @@
|
|||
mapContext.value.components.linesLayer.unhideLine(line.value.id);
|
||||
}
|
||||
};
|
||||
|
||||
toasts.showToast(`fm${context.id}-line-info-move`, `Edit waypoints`, "Use the routing form or drag the line around to change it. Click “Finish” to save the changes.", {
|
||||
noCloseButton: true,
|
||||
actions: [
|
||||
{ label: "Finish", variant: "primary", onClick: () => { done(true); }},
|
||||
{ label: "Cancel", onClick: () => { done(false); } }
|
||||
]
|
||||
});
|
||||
|
||||
isMoving.value = true;
|
||||
} catch (err) {
|
||||
toasts.showErrorToast(`fm${context.id}-line-info-move-error`, "Error saving line", err);
|
||||
|
||||
|
@ -232,11 +234,18 @@
|
|||
:disabled="isDeleting || mapContext.interaction"
|
||||
>
|
||||
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
|
||||
Remove
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<RouteForm v-if="isMoving" active ref="routeForm" :route-id="`l${line.id}`" :show-toolbar="false"></RouteForm>
|
||||
<RouteForm
|
||||
v-if="isMoving"
|
||||
active
|
||||
ref="routeForm"
|
||||
:routeId="`l${line.id}`"
|
||||
:showToolbar="false"
|
||||
noClear
|
||||
></RouteForm>
|
||||
|
||||
<EditLineDialog
|
||||
v-if="showEditDialog"
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
if (!await showConfirm({
|
||||
title: "Delete type",
|
||||
message: `Do you really want to delete the type “${type.name}”?`,
|
||||
variant: "danger"
|
||||
variant: "danger",
|
||||
okLabel: "Delete"
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
@ -45,7 +46,6 @@
|
|||
<template>
|
||||
<ModalDialog
|
||||
title="Manage Types"
|
||||
okOnly
|
||||
:isBusy="isBusy"
|
||||
size="lg"
|
||||
class="fm-manage-types"
|
||||
|
@ -64,23 +64,21 @@
|
|||
<td>{{type.name}}</td>
|
||||
<td>{{type.type}}</td>
|
||||
<td class="td-buttons">
|
||||
<div class="btn-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:disabled="isDeleting[type.id]"
|
||||
@click="editDialogTypeId = type.id"
|
||||
>Edit</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="deleteType(type)"
|
||||
class="btn btn-secondary"
|
||||
:disabled="isDeleting[type.id]"
|
||||
>
|
||||
<div v-if="isDeleting[type.id]" class="spinner-border spinner-border-sm"></div>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:disabled="isDeleting[type.id]"
|
||||
@click="editDialogTypeId = type.id"
|
||||
>Edit</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="deleteType(type)"
|
||||
class="btn btn-secondary"
|
||||
:disabled="isDeleting[type.id]"
|
||||
>
|
||||
<div v-if="isDeleting[type.id]" class="spinner-border spinner-border-sm"></div>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -97,6 +95,10 @@
|
|||
</tfoot>
|
||||
</table>
|
||||
|
||||
<EditTypeDialog v-if="editDialogTypeId !== undefined" :typeId="editDialogTypeId ?? undefined"></EditTypeDialog>
|
||||
<EditTypeDialog
|
||||
v-if="editDialogTypeId !== undefined"
|
||||
:typeId="editDialogTypeId ?? undefined"
|
||||
@hidden="editDialogTypeId = undefined"
|
||||
></EditTypeDialog>
|
||||
</ModalDialog>
|
||||
</template>
|
|
@ -44,7 +44,12 @@
|
|||
toasts.hideToast(`fm${context.id}-save-view-error-${view.id}`);
|
||||
|
||||
try {
|
||||
if (!await showConfirm({ title: "Delete view", message: `Do you really want to delete the view “${view.name}”?` }))
|
||||
if (!await showConfirm({
|
||||
title: "Delete view",
|
||||
message: `Do you really want to delete the view “${view.name}”?`,
|
||||
variant: "danger",
|
||||
okLabel: "Delete"
|
||||
}))
|
||||
return;
|
||||
|
||||
isDeleting.value.add(view.id);
|
||||
|
@ -61,7 +66,6 @@
|
|||
<template>
|
||||
<ModalDialog
|
||||
title="Manage Views"
|
||||
okOnly
|
||||
:isBusy="isBusy"
|
||||
size="lg"
|
||||
class="fm-manage-views"
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
function handleOpenSelection(): void {
|
||||
if (marker.value)
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-marker-info-tab`);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-marker-info-tab`, { expand: true });
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
import UseAsDropdown from "../ui/use-as-dropdown.vue";
|
||||
import ZoomToObjectButton from "../ui/zoom-to-object-button.vue";
|
||||
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import type { RouteDestination } from "../facil-map-context-provider/map-context";
|
||||
import type { RouteDestination } from "../facil-map-context-provider/route-form-tab-context";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const client = requireClientContext(context);
|
||||
|
@ -42,7 +42,12 @@
|
|||
async function deleteMarker(): Promise<void> {
|
||||
toasts.hideToast(`fm${context.id}-marker-info-delete`);
|
||||
|
||||
if (!await showConfirm({ title: "Remove marker", message: `Do you really want to remove the marker “${normalizeMarkerName(marker.value.name)}”?` }))
|
||||
if (!await showConfirm({
|
||||
title: "Delete marker",
|
||||
message: `Do you really want to delete the marker “${normalizeMarkerName(marker.value.name)}”?`,
|
||||
variant: "danger",
|
||||
okLabel: "Delete"
|
||||
}))
|
||||
return;
|
||||
|
||||
isDeleting.value = true;
|
||||
|
@ -124,7 +129,7 @@
|
|||
:disabled="isDeleting || mapContext.interaction"
|
||||
>
|
||||
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
|
||||
Remove
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
function handleOpenSelection(): void {
|
||||
if (objects.value)
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-multiple-info-tab`);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-multiple-info-tab`, { expand: true });
|
||||
}
|
||||
|
||||
const title = computed(() => objects.value ? `${objects.value.length} objects` : "");
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
import { useCarousel } from "../../utils/carousel";
|
||||
import ZoomToObjectButton from "../ui/zoom-to-object-button.vue";
|
||||
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import vTooltip from "../../utils/tooltip";
|
||||
import { normalizeLineName, normalizeMarkerName } from "facilmap-utils";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const client = requireClientContext(context);
|
||||
|
@ -64,7 +66,12 @@
|
|||
async function deleteObjects(): Promise<void> {
|
||||
toasts.hideToast(`fm${context.id}-multiple-info-delete`);
|
||||
|
||||
if (!props.objects || !await showConfirm({ title: "Delete objects", message: `Do you really want to remove ${props.objects.length} objects?` }))
|
||||
if (!props.objects || !await showConfirm({
|
||||
title: "Delete objects",
|
||||
message: `Do you really want to remove ${props.objects.length} objects?`,
|
||||
variant: "danger",
|
||||
okLabel: "Delete"
|
||||
}))
|
||||
return;
|
||||
|
||||
isDeleting.value = true;
|
||||
|
@ -97,12 +104,12 @@
|
|||
|
||||
<template>
|
||||
<div class="fm-multiple-info">
|
||||
<div class="carousel slide" ref="carouselRef">
|
||||
<div class="carousel slide fm-flex-carousel" ref="carouselRef">
|
||||
<div class="carousel-item" :class="{ active: carousel.tab === 0 }">
|
||||
<ul class="list-group">
|
||||
<ul class="list-group fm-search-box-collapse-point">
|
||||
<li v-for="object in props.objects" :key="`${isMarker(object) ? 'm' : 'l'}-${object.id}`" class="list-group-item active">
|
||||
<span>
|
||||
<a href="javascript:" @click="emit('click-object', object, $event)">{{object.name}}</a>
|
||||
<a href="javascript:" @click="emit('click-object', object, $event)">{{isMarker(object) ? normalizeMarkerName(object.name) : normalizeLineName(object.name)}}</a>
|
||||
{{" "}}
|
||||
<span class="result-type" v-if="client.types[object.typeId]">({{client.types[object.typeId].name}})</span>
|
||||
</span>
|
||||
|
@ -111,7 +118,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="btn-toolbar">
|
||||
<div class="btn-toolbar mt-2">
|
||||
<ZoomToObjectButton
|
||||
v-if="zoomDestination"
|
||||
label="selection"
|
||||
|
@ -127,7 +134,7 @@
|
|||
:disabled="isDeleting || mapContext.interaction"
|
||||
>
|
||||
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
|
||||
Remove
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -152,6 +159,10 @@
|
|||
|
||||
<style lang="scss">
|
||||
.fm-multiple-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -92,11 +92,16 @@
|
|||
<template>
|
||||
<div class="fm-overpass-form">
|
||||
<template v-if="!mapContext.overpassIsCustom">
|
||||
<input class="form-control" type="search" v-model="searchTerm" placeholder="Filter…" autofocus />
|
||||
<input
|
||||
class="form-control fm-autofocus"
|
||||
type="search"
|
||||
v-model="searchTerm"
|
||||
placeholder="Filter…"
|
||||
/>
|
||||
<hr />
|
||||
|
||||
<template v-if="searchTerm">
|
||||
<div class="checkbox-grid">
|
||||
<div class="checkbox-grid fm-search-box-collapse-point">
|
||||
<template v-for="preset in filteredPresets" :key="preset.key">
|
||||
<div class="form-check">
|
||||
<input
|
||||
|
@ -135,25 +140,29 @@
|
|||
</template>
|
||||
</ul>
|
||||
|
||||
<template v-for="(presets, idx) in categories[activeTab].presets" :key="idx">
|
||||
<hr />
|
||||
<div class="checkbox-grid">
|
||||
<template v-for="preset in presets" :key="preset.key">
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
:id="`fm${context.id}-overpass-form-preset-${preset.key}`"
|
||||
:checked="preset.isChecked"
|
||||
@change="togglePreset(preset.key, ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<label :for="`fm${context.id}-overpass-form-preset-${preset.key}`" class="form-check-label">
|
||||
{{preset.label}}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<hr />
|
||||
|
||||
<div class="fm-search-box-collapse-point">
|
||||
<template v-for="(presets, idx) in categories[activeTab].presets" :key="idx">
|
||||
<hr v-if="idx > 0" />
|
||||
<div class="checkbox-grid">
|
||||
<template v-for="preset in presets" :key="preset.key">
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
:id="`fm${context.id}-overpass-form-preset-${preset.key}`"
|
||||
:checked="preset.isChecked"
|
||||
@change="togglePreset(preset.key, ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<label :for="`fm${context.id}-overpass-form-preset-${preset.key}`" class="form-check-label">
|
||||
{{preset.label}}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
@ -200,6 +209,7 @@
|
|||
.fm-overpass-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
.checkbox-grid {
|
||||
column-width: 160px;
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
function handleOpenSelection(): void {
|
||||
if (elements.value.length > 0)
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-overpass-info-tab`);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-overpass-info-tab`, { expand: true });
|
||||
}
|
||||
|
||||
function handleElementClick(element: OverpassElement, event: MouseEvent): void {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import type { Type } from "facilmap-types";
|
||||
import UseAsDropdown from "../ui/use-as-dropdown.vue";
|
||||
import ZoomToObjectButton from "../ui/zoom-to-object-button.vue";
|
||||
import type { RouteDestination } from "../facil-map-context-provider/map-context";
|
||||
import type { RouteDestination } from "../facil-map-context-provider/route-form-tab-context";
|
||||
import { overpassElementsToMarkersWithTags } from "../../utils/add";
|
||||
import AddToMapDropdown from "../ui/add-to-map-dropdown.vue";
|
||||
|
||||
|
@ -63,6 +63,7 @@
|
|||
<AddToMapDropdown
|
||||
:markers="markersWithTags"
|
||||
size="sm"
|
||||
isSingle
|
||||
></AddToMapDropdown>
|
||||
|
||||
<UseAsDropdown
|
||||
|
|
|
@ -75,7 +75,8 @@
|
|||
if (!await showConfirm({
|
||||
title: "Delete map",
|
||||
message: `Are you sure you want to delete the map “${padData.value.name}”? Deleted maps cannot be restored!`,
|
||||
variant: "danger"
|
||||
variant: "danger",
|
||||
okLabel: "Delete map"
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -2,33 +2,39 @@
|
|||
import RouteForm from "./route-form.vue";
|
||||
import type { HashQuery } from "facilmap-leaflet";
|
||||
import SearchBoxTab from "../search-box/search-box-tab.vue";
|
||||
import { ref } from "vue";
|
||||
import { useEventListener } from "../../utils/utils";
|
||||
import { injectContextRequired, requireMapContext, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import { readonly, ref, toRef } from "vue";
|
||||
import { injectContextRequired, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import type { WritableRouteFormTabContext } from "../facil-map-context-provider/route-form-tab-context";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const mapContext = requireMapContext(context);
|
||||
const searchBoxContext = requireSearchBoxContext(context);
|
||||
|
||||
const routeForm = ref<InstanceType<typeof RouteForm>>();
|
||||
|
||||
const hashQuery = ref<HashQuery>();
|
||||
|
||||
useEventListener(mapContext, "route-set-query", (data) => {
|
||||
routeForm.value!.setQuery(data);
|
||||
});
|
||||
useEventListener(mapContext, "route-set-from", (data) => {
|
||||
routeForm.value!.setFrom(data);
|
||||
});
|
||||
useEventListener(mapContext, "route-add-via", (data) => {
|
||||
routeForm.value!.addVia(data);
|
||||
});
|
||||
useEventListener(mapContext, "route-set-to", (data) => {
|
||||
routeForm.value!.setTo(data);
|
||||
const routeFormTabContext = ref<WritableRouteFormTabContext>({
|
||||
setQuery(query, zoom, smooth) {
|
||||
routeForm.value!.setQuery(query, zoom, smooth);
|
||||
},
|
||||
|
||||
setFrom(destination) {
|
||||
routeForm.value!.setFrom(destination);
|
||||
},
|
||||
|
||||
addVia(destination) {
|
||||
routeForm.value!.addVia(destination);
|
||||
},
|
||||
|
||||
setTo(destination) {
|
||||
routeForm.value!.setTo(destination);
|
||||
}
|
||||
});
|
||||
|
||||
context.provideComponent("routeFormTab", toRef(readonly(routeFormTabContext)));
|
||||
|
||||
function activate(): void {
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-route-form-tab`);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-route-form-tab`, { expand: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import vTooltip from "../../utils/tooltip";
|
||||
import DropdownMenu from "../ui/dropdown-menu.vue";
|
||||
import ZoomToObjectButton from "../ui/zoom-to-object-button.vue";
|
||||
import type { RouteDestination } from "../facil-map-context-provider/map-context";
|
||||
import type { RouteDestination } from "../facil-map-context-provider/route-form-tab-context";
|
||||
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import AddToMapDropdown from "../ui/add-to-map-dropdown.vue";
|
||||
|
||||
|
@ -83,9 +83,11 @@
|
|||
const submitButton = ref<HTMLButtonElement>();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** If false, the route layer will be opaque and not draggable. */
|
||||
active?: boolean;
|
||||
routeId?: string;
|
||||
showToolbar?: boolean;
|
||||
noClear?: boolean;
|
||||
}>(), {
|
||||
active: true,
|
||||
showToolbar: true
|
||||
|
@ -206,6 +208,10 @@
|
|||
return undefined;
|
||||
});
|
||||
|
||||
const destinationsMeta = computed(() => destinations.value.map((destination) => ({
|
||||
isInvalid: getValidationState(destination) === false
|
||||
})));
|
||||
|
||||
watchEffect(() => {
|
||||
if (hasRoute.value)
|
||||
routeLayer.setStyle({ opacity: props.active ? 1 : 0.35, raised: props.active });
|
||||
|
@ -502,7 +508,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
function setQuery({ query, zoom = true, smooth = true }: { query: string; zoom?: boolean; smooth?: boolean }): void {
|
||||
function setQuery(query: string, zoom = true, smooth = true): void {
|
||||
clear();
|
||||
const split = splitRouteQuery(query);
|
||||
destinations.value = split.queries.map((query) => ({ query }));
|
||||
|
@ -546,7 +552,6 @@
|
|||
class="input-group"
|
||||
@mouseenter="destinationMouseOver(idx)"
|
||||
@mouseleave="destinationMouseOut(idx)"
|
||||
:state="getValidationState(destination)"
|
||||
>
|
||||
<span class="input-group-text px-2">
|
||||
<a href="javascript:" class="fm-drag-handle" @contextmenu.prevent>
|
||||
|
@ -558,7 +563,8 @@
|
|||
v-model="destination.query"
|
||||
:placeholder="idx == 0 ? 'From' : idx == destinations.length-1 ? 'To' : 'Via'"
|
||||
:tabindex="idx+1"
|
||||
:state="getValidationState(destination)"
|
||||
:class="{ 'is-invalid': destinationsMeta[idx].isInvalid }"
|
||||
:autofocus="idx === 0"
|
||||
@blur="loadSuggestions(destination)"
|
||||
/>
|
||||
<template v-if="destination.query.trim() != ''">
|
||||
|
@ -651,7 +657,7 @@
|
|||
ref="submitButton"
|
||||
>Go!</button>
|
||||
<button
|
||||
v-if="hasRoute"
|
||||
v-if="hasRoute && !props.noClear"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:tabindex="destinations.length+8"
|
||||
|
@ -694,6 +700,7 @@
|
|||
<AddToMapDropdown
|
||||
:lines="linesWithTags"
|
||||
size="sm"
|
||||
isSingle
|
||||
></AddToMapDropdown>
|
||||
|
||||
<DropdownMenu
|
||||
|
@ -742,6 +749,10 @@
|
|||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.destination:first-child {
|
||||
margin-top: calc(-0.5rem + 2px); // Offset space of first fm-route-form-hover-insert
|
||||
}
|
||||
|
||||
hr.fm-route-form-hover-insert {
|
||||
margin: 0.1rem -0.5rem;
|
||||
width: auto;
|
||||
|
|
|
@ -177,7 +177,7 @@
|
|||
<div class="row mb-3">
|
||||
<label :for="`${id}-filter-input`" class="col-sm-3 col-form-label">Filter</label>
|
||||
<div class="col-sm-9">
|
||||
<input class="form-control" :id="`${id}-filter-input`" value="—" plaintext />
|
||||
<input class="form-control-plaintext" :id="`${id}-filter-input`" value="—" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
import hammer from "hammerjs";
|
||||
import { type Ref, defineComponent, nextTick, onMounted, onScopeDispose, reactive, readonly, ref, toRef, watch } from "vue";
|
||||
import vTooltip, { hideAllTooltips } from "../../utils/tooltip";
|
||||
import { useEventListener } from "../../utils/utils";
|
||||
import type { SearchBoxEventMap, SearchBoxTab, WritableSearchBoxContext } from "../facil-map-context-provider/search-box-context";
|
||||
import mitt from "mitt";
|
||||
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
|
@ -26,7 +25,7 @@
|
|||
}, { immediate: true });
|
||||
|
||||
if (activeTabId.value == null) {
|
||||
activateTab(id);
|
||||
activateTab(id, { autofocus: context.settings.autofocus });
|
||||
}
|
||||
|
||||
onScopeDispose(() => {
|
||||
|
@ -36,17 +35,28 @@
|
|||
tabHistory.value = tabHistory.value.filter((v) => v !== id);
|
||||
if (isActive) {
|
||||
activeTabId.value = tabHistory.value[tabHistory.value.length - 1];
|
||||
if (restoreHeight.value && context.isNarrow && containerRef.value) {
|
||||
containerRef.value.style.flexBasis = `${restoreHeight.value}px`;
|
||||
}
|
||||
restoreHeight.value = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function activateTab(id: string, expand?: boolean) {
|
||||
function activateTab(id: string, { expand = false, autofocus = false }: { expand?: boolean; autofocus?: boolean } = {}) {
|
||||
activeTabId.value = id;
|
||||
tabHistory.value.push(id);
|
||||
restoreHeight.value = undefined;
|
||||
|
||||
if (expand) {
|
||||
nextTick(() => {
|
||||
searchBoxContext.emit("expand");
|
||||
doExpand();
|
||||
});
|
||||
}
|
||||
|
||||
if (autofocus && !context.isNarrow) {
|
||||
nextTick(() => {
|
||||
containerRef.value?.querySelector<HTMLElement>(":scope > .card-body.active [autofocus],:scope > .card-body.active .fm-autofocus")?.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -81,8 +91,6 @@
|
|||
const hasFocus = ref(false);
|
||||
const isResizing = ref(false);
|
||||
|
||||
useEventListener(toRef(searchBoxContext), "expand", handleExpand);
|
||||
|
||||
onMounted(() => {
|
||||
const pan = new hammer.Manager(cardHeaderRef.value!);
|
||||
pan.add(new hammer.Pan({ direction: hammer.DIRECTION_VERTICAL }));
|
||||
|
@ -120,7 +128,7 @@
|
|||
return Math.max(0, Math.min(maxHeight, height));
|
||||
}
|
||||
|
||||
function handleExpand(): void {
|
||||
function doExpand(): void {
|
||||
if (context.isNarrow) {
|
||||
const currentHeight = parseInt(getComputedStyle(containerRef.value!).flexBasis);
|
||||
if (currentHeight < 120) {
|
||||
|
@ -210,7 +218,7 @@
|
|||
:aria-current="tabId === searchBoxContext.activeTabId ? 'true' : undefined"
|
||||
href="javascript:"
|
||||
:class="{ active: tabId === searchBoxContext.activeTabId }"
|
||||
@click="searchBoxContext.activateTab(tabId, true)"
|
||||
@click="searchBoxContext.activateTab(tabId, { expand: true, autofocus: true })"
|
||||
>{{tab.title}}</a>
|
||||
|
||||
<a
|
||||
|
@ -223,7 +231,7 @@
|
|||
</div>
|
||||
|
||||
<template v-for="[tabId, tab] in searchBoxContext.tabs" :key="tabId">
|
||||
<div v-show="tabId === searchBoxContext.activeTabId" class="card-body" :class="tab.class">
|
||||
<div v-show="tabId === searchBoxContext.activeTabId" class="card-body" :class="[tab.class, { active: tabId === searchBoxContext.activeTabId }]">
|
||||
<TabContent :tabId="tabId" :isActive="tabId === searchBoxContext.activeTabId"></TabContent>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -301,6 +309,8 @@
|
|||
}
|
||||
|
||||
> .card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
@ -347,6 +357,7 @@
|
|||
|
||||
hr {
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
import SearchForm from "./search-form.vue";
|
||||
import { Util } from "leaflet";
|
||||
import type { HashQuery } from "facilmap-leaflet";
|
||||
import { ref } from "vue";
|
||||
import { readonly, ref, toRef } from "vue";
|
||||
import { useEventListener } from "../../utils/utils";
|
||||
import SearchBoxTab from "../search-box/search-box-tab.vue";
|
||||
import { injectContextRequired, requireMapContext, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import type { WritableSearchFormTabContext } from "../facil-map-context-provider/search-form-tab-context";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const mapContext = requireMapContext(context);
|
||||
|
@ -16,19 +17,22 @@
|
|||
const hashQuery = ref<HashQuery | undefined>(undefined);
|
||||
|
||||
useEventListener(mapContext, "open-selection", handleOpenSelection);
|
||||
useEventListener(mapContext, "search-set-query", handleSetQuery);
|
||||
|
||||
function handleOpenSelection(): void {
|
||||
const layerId = Util.stamp(mapContext.value.components.searchResultsLayer);
|
||||
if (mapContext.value.selection.some((item) => item.type == "searchResult" && item.layerId == layerId))
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-search-form-tab`);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-search-form-tab`, { expand: true });
|
||||
}
|
||||
|
||||
function handleSetQuery({ query, zoom = false, smooth = true }: { query: string; zoom?: boolean; smooth?: boolean }): void {
|
||||
searchForm.value!.setSearchString(query);
|
||||
searchForm.value!.search(zoom, undefined, smooth);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-search-form-tab`, !!query);
|
||||
}
|
||||
const searchFormTabContext: WritableSearchFormTabContext = {
|
||||
setQuery(query, zoom = false, smooth = true, autofocus = false): void {
|
||||
searchForm.value!.setSearchString(query);
|
||||
searchForm.value!.search(zoom, undefined, smooth);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-search-form-tab`, { expand: !!query, autofocus });
|
||||
}
|
||||
};
|
||||
|
||||
context.provideComponent("searchFormTab", toRef(readonly(searchFormTabContext)));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
|
||||
const toasts = useToasts();
|
||||
|
||||
const autofocus = ref(!context.isNarrow && context.settings.autofocus);
|
||||
const layerId = Util.stamp(mapContext.value.components.searchResultsLayer);
|
||||
|
||||
const searchInput = ref<HTMLInputElement>();
|
||||
|
@ -173,7 +172,7 @@
|
|||
<div class="fm-search-form">
|
||||
<form action="javascript:" @submit.prevent="handleSubmit()">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" v-model="searchString" :autofocus="autofocus" ref="searchInput" />
|
||||
<input type="search" class="form-control fm-autofocus" v-model="searchString" ref="searchInput" />
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary"
|
||||
|
@ -218,7 +217,6 @@
|
|||
:auto-zoom="storage.autoZoom"
|
||||
:union-zoom="storage.zoomToAll"
|
||||
:layer-id="layerId"
|
||||
class="fm-search-box-collapse-point"
|
||||
/>
|
||||
<SearchResults
|
||||
v-else-if="searchResults || mapResults"
|
||||
|
@ -227,7 +225,6 @@
|
|||
:auto-zoom="storage.autoZoom"
|
||||
:union-zoom="storage.zoomToAll"
|
||||
:layer-id="layerId"
|
||||
class="fm-search-box-collapse-point"
|
||||
></SearchResults>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import { computed } from "vue";
|
||||
import UseAsDropdown from "./ui/use-as-dropdown.vue";
|
||||
import ZoomToObjectButton from "./ui/zoom-to-object-button.vue";
|
||||
import type { RouteDestination } from "./facil-map-context-provider/map-context";
|
||||
import type { RouteDestination } from "./facil-map-context-provider/route-form-tab-context";
|
||||
import AddToMapDropdown from "./ui/add-to-map-dropdown.vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
@ -89,27 +89,26 @@
|
|||
</template>
|
||||
</dl>
|
||||
|
||||
<div>
|
||||
<div class="btn-toolbar" role="group">
|
||||
<ZoomToObjectButton
|
||||
v-if="zoomDestination"
|
||||
label="search result"
|
||||
size="sm"
|
||||
:destination="zoomDestination"
|
||||
></ZoomToObjectButton>
|
||||
<div class="btn-toolbar">
|
||||
<ZoomToObjectButton
|
||||
v-if="zoomDestination"
|
||||
label="search result"
|
||||
size="sm"
|
||||
:destination="zoomDestination"
|
||||
></ZoomToObjectButton>
|
||||
|
||||
<AddToMapDropdown
|
||||
:markers="markersWithTags"
|
||||
:lines="linesWithTags"
|
||||
size="sm"
|
||||
></AddToMapDropdown>
|
||||
<AddToMapDropdown
|
||||
:markers="markersWithTags"
|
||||
:lines="linesWithTags"
|
||||
size="sm"
|
||||
isSingle
|
||||
></AddToMapDropdown>
|
||||
|
||||
<UseAsDropdown
|
||||
v-if="isMarker && routeDestination"
|
||||
size="sm"
|
||||
:destination="routeDestination"
|
||||
></UseAsDropdown>
|
||||
</div>
|
||||
<UseAsDropdown
|
||||
v-if="isMarker && routeDestination"
|
||||
size="sm"
|
||||
:destination="routeDestination"
|
||||
></UseAsDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import { useCarousel } from "../../utils/carousel";
|
||||
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import AddToMapDropdown from "../ui/add-to-map-dropdown.vue";
|
||||
import { normalizeLineName, normalizeMarkerName } from "facilmap-utils";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const client = requireClientContext(context);
|
||||
|
@ -124,7 +125,7 @@
|
|||
if (isMapResult(result)) {
|
||||
if (result.kind == "marker" && !client.value.markers[result.id])
|
||||
await client.value.getMarker({ id: result.id });
|
||||
searchBoxContext.value?.activateTab(`fm${context.id}-${result.kind}-info-tab`, false);
|
||||
searchBoxContext.value?.activateTab(`fm${context.id}-${result.kind}-info-tab`);
|
||||
} else
|
||||
carousel.setTab(1);
|
||||
}, 0);
|
||||
|
@ -158,61 +159,63 @@
|
|||
|
||||
<template>
|
||||
<div class="fm-search-results" :class="{ isNarrow: context.isNarrow }">
|
||||
<div class="carousel slide" ref="carouselRef">
|
||||
<div class="carousel slide fm-flex-carousel" ref="carouselRef">
|
||||
<div class="carousel-item" :class="{ active: carousel.tab === 0 }">
|
||||
<div
|
||||
v-if="(!searchResults || searchResults.length == 0) && (!mapResults || mapResults.length == 0)"
|
||||
class="alert alert-danger"
|
||||
>
|
||||
No results have been found.
|
||||
<div class="fm-search-box-collapse-point">
|
||||
<div
|
||||
v-if="(!searchResults || searchResults.length == 0) && (!mapResults || mapResults.length == 0)"
|
||||
class="alert alert-danger"
|
||||
>
|
||||
No results have been found.
|
||||
</div>
|
||||
|
||||
<slot name="before"></slot>
|
||||
|
||||
<ul v-if="mapResults && mapResults.length > 0" class="list-group">
|
||||
<!-- eslint-disable-next-line vue/require-v-for-key -->
|
||||
<li
|
||||
v-for="result in mapResults"
|
||||
class="list-group-item"
|
||||
:class="{ active: activeResults.includes(result) }"
|
||||
v-scroll-into-view="activeResults.includes(result)"
|
||||
>
|
||||
<span>
|
||||
<a href="javascript:" @click="handleClick(result, $event)">{{isMarkerResult(result) ? normalizeMarkerName(result.name) : normalizeLineName(result.name)}}</a>
|
||||
{{" "}}
|
||||
<span class="result-type">({{client.types[result.typeId].name}})</span>
|
||||
</span>
|
||||
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.hover.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
|
||||
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.left="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr v-if="mapResults && mapResults.length > 0 && searchResults && searchResults.length > 0"/>
|
||||
|
||||
<ul v-if="searchResults && searchResults.length > 0" class="list-group">
|
||||
<!-- eslint-disable-next-line vue/require-v-for-key -->
|
||||
<li
|
||||
v-for="result in searchResults"
|
||||
class="list-group-item"
|
||||
:class="{ active: activeResults.includes(result) }"
|
||||
v-scroll-into-view="activeResults.includes(result)"
|
||||
>
|
||||
<span>
|
||||
<a href="javascript:" @click="handleClick(result, $event)">{{result.display_name}}</a>
|
||||
{{" "}}
|
||||
<span class="result-type" v-if="result.type">({{result.type}})</span>
|
||||
</span>
|
||||
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
|
||||
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.right="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
|
||||
<slot name="before"></slot>
|
||||
|
||||
<ul v-if="mapResults && mapResults.length > 0" class="list-group">
|
||||
<!-- eslint-disable-next-line vue/require-v-for-key -->
|
||||
<li
|
||||
v-for="result in mapResults"
|
||||
class="list-group-item"
|
||||
:class="{ active: activeResults.includes(result) }"
|
||||
v-scroll-into-view="activeResults.includes(result)"
|
||||
>
|
||||
<span>
|
||||
<a href="javascript:" @click="handleClick(result, $event)">{{result.name}}</a>
|
||||
{{" "}}
|
||||
<span class="result-type">({{client.types[result.typeId].name}})</span>
|
||||
</span>
|
||||
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.hover.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
|
||||
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.left="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr v-if="mapResults && mapResults.length > 0 && searchResults && searchResults.length > 0"/>
|
||||
|
||||
<ul v-if="searchResults && searchResults.length > 0" class="list-group">
|
||||
<!-- eslint-disable-next-line vue/require-v-for-key -->
|
||||
<li
|
||||
v-for="result in searchResults"
|
||||
class="list-group-item"
|
||||
:class="{ active: activeResults.includes(result) }"
|
||||
v-scroll-into-view="activeResults.includes(result)"
|
||||
>
|
||||
<span>
|
||||
<a href="javascript:" @click="handleClick(result, $event)">{{result.display_name}}</a>
|
||||
{{" "}}
|
||||
<span class="result-type" v-if="result.type">({{result.type}})</span>
|
||||
</span>
|
||||
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
|
||||
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.right="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<slot name="after"></slot>
|
||||
|
||||
<div v-if="client.padData && !client.readonly && searchResults && searchResults.length > 0" class="btn-toolbar">
|
||||
<div v-if="client.padData && !client.readonly && searchResults && searchResults.length > 0" class="btn-toolbar mt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
class="btn btn-secondary btn-sm"
|
||||
:class="{ active: isAllSelected }"
|
||||
@click="toggleSelectAll"
|
||||
>Select all</button>
|
||||
|
@ -221,6 +224,7 @@
|
|||
:label="`Add selected item${activeSearchResults.length == 1 ? '' : 's'} to map`"
|
||||
:markers="activeMarkersWithTags"
|
||||
:lines="activeLinesWithTags"
|
||||
size="sm"
|
||||
>
|
||||
<template v-if="hasCustomTypes" #after>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
@ -259,6 +263,10 @@
|
|||
|
||||
<style lang="scss">
|
||||
.fm-search-results.fm-search-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { useToasts } from "../ui/toasts/toasts.vue";
|
||||
import DropdownMenu from "../ui/dropdown-menu.vue";
|
||||
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import { sleep } from "facilmap-utils";
|
||||
|
||||
const emit = defineEmits<{
|
||||
"hide-sidebar": [];
|
||||
|
@ -21,19 +22,13 @@
|
|||
| "manage-types"
|
||||
>();
|
||||
|
||||
function addObject(type: Type): void {
|
||||
if(type.type == "marker")
|
||||
addMarker(type);
|
||||
else if(type.type == "line")
|
||||
addLine(type);
|
||||
}
|
||||
|
||||
function addMarker(type: Type): void {
|
||||
drawMarker(type, context, toasts);
|
||||
}
|
||||
|
||||
function addLine(type: Type): void {
|
||||
drawLine(type, context, toasts);
|
||||
async function addObject(type: Type): Promise<void> {
|
||||
if(type.type == "marker") {
|
||||
await sleep(0); // For some reason this is necessary for the dropdown to close itself
|
||||
drawMarker(type, context, toasts);
|
||||
} else if(type.type == "line") {
|
||||
drawLine(type, context, toasts);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import PadSettingsDialog from "../pad-settings-dialog/pad-settings-dialog.vue";
|
||||
import EditFilter from "../edit-filter-dialog.vue";
|
||||
import EditFilterDialog from "../edit-filter-dialog.vue";
|
||||
import HistoryDialog from "../history-dialog/history-dialog.vue";
|
||||
import ShareDialog from "../share-dialog.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, toRef } from "vue";
|
||||
import vTooltip from "../../utils/tooltip";
|
||||
import DropdownMenu from "../ui/dropdown-menu.vue";
|
||||
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
|
@ -11,6 +11,7 @@
|
|||
const context = injectContextRequired();
|
||||
const client = requireClientContext(context);
|
||||
const mapContext = requireMapContext(context);
|
||||
const importTabContext = toRef(() => context.components.importTab);
|
||||
|
||||
const props = defineProps<{
|
||||
interactive: boolean;
|
||||
|
@ -38,10 +39,6 @@
|
|||
return { q: "", a: "" };
|
||||
}
|
||||
});
|
||||
|
||||
function importFile(): void {
|
||||
mapContext.value.emit("import-file");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -61,11 +58,11 @@
|
|||
>Share</a>
|
||||
</li>
|
||||
|
||||
<li v-if="props.interactive">
|
||||
<li v-if="props.interactive && importTabContext">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="javascript:"
|
||||
@click="importFile(); emit('hide-sidebar')"
|
||||
@click="importTabContext.openFilePicker(); emit('hide-sidebar')"
|
||||
>Open file</a>
|
||||
</li>
|
||||
|
||||
|
@ -143,10 +140,10 @@
|
|||
@hidden="dialog = undefined"
|
||||
></ShareDialog>
|
||||
|
||||
<EditFilter
|
||||
<EditFilterDialog
|
||||
v-if="dialog === 'edit-filter' && client.padData"
|
||||
@hidden="dialog = undefined"
|
||||
></EditFilter>
|
||||
></EditFilterDialog>
|
||||
|
||||
<HistoryDialog
|
||||
v-if="dialog === 'history' && client.padData"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import type { SelectedItem } from '../../utils/selection';
|
||||
import { type LineWithTags, type MarkerWithTags, addToMap } from '../../utils/add';
|
||||
import type { ButtonSize } from '../../utils/bootstrap';
|
||||
import DropdownMenu from "./dropdown-menu.vue";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const client = requireClientContext(context);
|
||||
|
@ -17,6 +18,8 @@
|
|||
lines?: LineWithTags[];
|
||||
label?: string;
|
||||
size?: ButtonSize;
|
||||
/** If true, the markers/lines entries are assumed to refer to a single object, omitting the prefix "Marker/line/polgon items as" */
|
||||
isSingle?: boolean;
|
||||
}>(), {
|
||||
label: "Add to map"
|
||||
})
|
||||
|
@ -82,7 +85,7 @@
|
|||
href="javascript:"
|
||||
class="dropdown-item"
|
||||
@click="addMarkers(type)"
|
||||
>{{props.lines ? 'Marker items as ' : ''}}{{type.name}}</a>
|
||||
>{{!props.isSingle && props.lines ? 'Marker items as ' : ''}}{{type.name}}</a>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -93,7 +96,7 @@
|
|||
href="javascript:"
|
||||
class="dropdown-item"
|
||||
@click="addLines(type)"
|
||||
>{{props.markers ? 'Line/polygon items as ' : ''}}{{type.name}}</a>
|
||||
>{{!props.isSingle && props.markers ? 'Line/polygon items as ' : ''}}{{type.name}}</a>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import { computed, createApp, defineComponent, h, ref, type VNode, type VNodeArrayChildren, withDirectives } from "vue";
|
||||
import Alert from "./alert.vue";
|
||||
import vValidity from "./validated-form/validity";
|
||||
import { useModal } from "../../utils/modal";
|
||||
import type { ThemeColour } from "../../utils/bootstrap";
|
||||
import ModalDialog from "./modal-dialog.vue";
|
||||
|
||||
export type AlertProps = {
|
||||
title: string;
|
||||
|
@ -11,6 +11,8 @@
|
|||
type?: "alert" | "confirm";
|
||||
variant?: ThemeColour;
|
||||
show?: boolean;
|
||||
okLabel?: string;
|
||||
okFocus?: boolean;
|
||||
};
|
||||
|
||||
export interface AlertResult {
|
||||
|
@ -45,16 +47,16 @@
|
|||
});
|
||||
}
|
||||
|
||||
export async function showAlert(props: Omit<AlertProps, 'type' | 'show'>): Promise<void> {
|
||||
await renderAlert({ ...props, type: 'alert' });
|
||||
export async function showAlert(props: Omit<AlertProps, 'type' | 'show' | 'okFocus'>): Promise<void> {
|
||||
await renderAlert({ ...props, okFocus: true, type: 'alert' });
|
||||
}
|
||||
|
||||
export async function showConfirm(props: Omit<AlertProps, 'type' | 'show'>): Promise<boolean> {
|
||||
const result = await renderAlert({ ...props, type: 'confirm' });
|
||||
export async function showConfirm(props: Omit<AlertProps, 'type' | 'show' | 'okFocus'>): Promise<boolean> {
|
||||
const result = await renderAlert({ ...props, okFocus: true, type: 'confirm' });
|
||||
return result.ok;
|
||||
}
|
||||
|
||||
export async function showPrompt({ initialValue = "", validate, ...props }: Omit<AlertProps, 'type' | 'show' | 'message'> & {
|
||||
export async function showPrompt({ initialValue = "", validate, ...props }: Omit<AlertProps, 'type' | 'show' | 'message' | 'okFocus'> & {
|
||||
initialValue?: string;
|
||||
/** Validate the value. Return an empty string or undefined to indicate validity. */
|
||||
validate?: (value: string) => string | undefined;
|
||||
|
@ -68,6 +70,7 @@
|
|||
const result = await renderAlert({
|
||||
...props,
|
||||
message: '',
|
||||
okFocus: false,
|
||||
type: 'confirm',
|
||||
getContent: () => h('div', {
|
||||
class: touched.value ? 'was-validated' : ''
|
||||
|
@ -75,7 +78,7 @@
|
|||
withDirectives(
|
||||
h('input', {
|
||||
type: "text",
|
||||
class: `form-control${touched.value ? ' was-validated' : ''}`,
|
||||
class: "form-control",
|
||||
value: value.value,
|
||||
onInput: (e: InputEvent) => {
|
||||
value.value = (e.target as HTMLInputElement).value;
|
||||
|
@ -107,7 +110,9 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<AlertProps>(), {
|
||||
type: "alert"
|
||||
type: "alert",
|
||||
okLabel: "OK",
|
||||
okFocus: true
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -120,49 +125,27 @@
|
|||
ok: false
|
||||
});
|
||||
|
||||
const modalRef = ref<HTMLElement>();
|
||||
const modal = useModal(modalRef, {
|
||||
onShown: () => {
|
||||
emit('shown');
|
||||
},
|
||||
onHide: () => {
|
||||
emit('hide', result.value);
|
||||
},
|
||||
onHidden: () => {
|
||||
emit('hidden', result.value);
|
||||
}
|
||||
});
|
||||
const modalRef = ref<InstanceType<typeof ModalDialog>>();
|
||||
|
||||
const formRef = ref<HTMLFormElement>();
|
||||
const formTouched = ref(false);
|
||||
const handleSubmit = () => {
|
||||
if (formRef.value!.checkValidity()) {
|
||||
result.value.ok = true;
|
||||
modal.hide();
|
||||
} else {
|
||||
formTouched.value = true;
|
||||
}
|
||||
result.value.ok = true;
|
||||
modalRef.value!.modal.hide();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="modal fade" tabindex="-1" aria-hidden="true" ref="modalRef">
|
||||
<div class="modal-dialog">
|
||||
<form class="modal-content" :class="{ 'was-validated': formTouched }" @submit.prevent="handleSubmit()" novalidate ref="formRef">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">{{props.title}}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot>{{props.message}}</slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button v-if="type === 'confirm'" type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn" :class="`btn-${props.variant ?? 'primary'}`">OK</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
<ModalDialog
|
||||
:title="props.title"
|
||||
:isCreate="props.type === 'confirm'"
|
||||
:okLabel="props.okLabel"
|
||||
:okVariant="props.variant"
|
||||
:okFocus="props.okFocus"
|
||||
@shown="emit('shown')"
|
||||
@hide="emit('hide', result)"
|
||||
@hidden="emit('hidden', result)"
|
||||
@submit="handleSubmit"
|
||||
ref="modalRef"
|
||||
>
|
||||
<slot>{{props.message}}</slot>
|
||||
</ModalDialog>
|
||||
</template>
|
|
@ -15,7 +15,7 @@
|
|||
}
|
||||
|
||||
function validateColour(colour: string): string | undefined {
|
||||
if (!isValidColour(colour)) {
|
||||
if (colour && !isValidColour(colour)) {
|
||||
return "Needs to be in 3-digit or 6-digit hex format, for example f00 or 0000ff.";
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,12 @@
|
|||
<Hue :value="val" @change="handleChange"></Hue>
|
||||
<ul ref="gridRef">
|
||||
<li v-for="colour in colours" :key="colour" :class="{ active: value == colour }">
|
||||
<a href="javascript:" :style="{ backgroundColor: `#${colour}` }" @click="emit('update:modelValue', colour)"></a>
|
||||
<a
|
||||
href="javascript:"
|
||||
:style="{ backgroundColor: `#${colour}` }"
|
||||
@click="emit('update:modelValue', colour)"
|
||||
tabindex="-1"
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -61,6 +61,8 @@
|
|||
}
|
||||
|
||||
watch(() => buttonRef.value?.elementRef, (newRef, oldRef, onCleanup) => {
|
||||
onCleanup(() => {}); // TODO: Delete me https://github.com/vuejs/core/issues/5151#issuecomment-1515613484
|
||||
|
||||
if (newRef) {
|
||||
dropdownRef.value = new CustomDropdown(newRef, {
|
||||
popperConfig: (defaultConfig) => ({
|
||||
|
@ -138,6 +140,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.isDisabled || props.isBusy) {
|
||||
dropdownRef.value?.hide();
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -18,12 +18,17 @@
|
|||
customClass?: string;
|
||||
/** If true, the width of the popover will be fixed to the width of the element. */
|
||||
enforceElementWidth?: boolean;
|
||||
/** If true, a click of the reference element will not toggle the popover. */
|
||||
ignoreClick?: boolean;
|
||||
}>(), {
|
||||
show: undefined
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:show": [show: boolean];
|
||||
shown: [];
|
||||
hide: [];
|
||||
hidden: [];
|
||||
}>();
|
||||
|
||||
const show = useRefWithOverride(false, () => props.show, (show) => {
|
||||
|
@ -35,7 +40,7 @@
|
|||
const shouldUseModal = useMaxBreakpoint("xs");
|
||||
|
||||
watch(show, () => {
|
||||
if (!show) {
|
||||
if (!show.value) {
|
||||
showModal.value = false;
|
||||
showPopover.value = false;
|
||||
} else if (!showModal.value && !showPopover.value) {
|
||||
|
@ -51,9 +56,16 @@
|
|||
|
||||
const modalRef = ref<HTMLElement>();
|
||||
const modal = useModal(modalRef, {
|
||||
onShown: () => {
|
||||
emit("shown");
|
||||
},
|
||||
onHide: () => {
|
||||
emit("hide");
|
||||
},
|
||||
onHidden: () => {
|
||||
showModal.value = false;
|
||||
show.value = false;
|
||||
emit("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -63,7 +75,9 @@
|
|||
};
|
||||
|
||||
const handleClick = () => {
|
||||
show.value = !show.value;
|
||||
if (!props.ignoreClick) {
|
||||
show.value = !show.value;
|
||||
}
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
|
@ -72,7 +86,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bb-popover">
|
||||
<div class="fm-hybrid-popover">
|
||||
<div ref="trigger" @click="handleClick()">
|
||||
<slot name="trigger"></slot>
|
||||
</div>
|
||||
|
@ -84,6 +98,9 @@
|
|||
:class="props.customClass"
|
||||
hideOnOutsideClick
|
||||
:enforceElementWidth="props.enforceElementWidth"
|
||||
@shown="emit('shown')"
|
||||
@hide="emit('hide')"
|
||||
@hidden="emit('hidden')"
|
||||
>
|
||||
<template v-slot:header>
|
||||
{{props.title}}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { useModal } from "../../utils/modal";
|
||||
import ValidatedForm, { type CustomSubmitEvent } from "./validated-form/validated-form.vue";
|
||||
import { reactiveReadonlyView } from "../../utils/vue";
|
||||
import type { ThemeColour } from "../../utils/bootstrap";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string;
|
||||
|
@ -12,31 +13,62 @@
|
|||
isBusy?: boolean;
|
||||
/** If true, the Save button will always be shown (also if isModified is false). */
|
||||
isCreate?: boolean;
|
||||
/** If false, the Save button will be hidden (unless noCancel is true). */
|
||||
/** If false, a Close button will be shown instead of a Save button (except is isCreate is true). */
|
||||
isModified?: boolean;
|
||||
size?: "sm" | "default" | "lg" | "xl";
|
||||
/**
|
||||
* If specified, the dialog will be shrunk a bit to make the underlying dialog partly visible in case of stacked dialogs.
|
||||
* 0 represents a non-stacked dialog (default), 1 should be the first stacked dialog, ...
|
||||
*/
|
||||
stackLevel?: number;
|
||||
okLabel?: string;
|
||||
okVariant?: ThemeColour;
|
||||
/** If true, the OK button is focused when the dialog is opened. */
|
||||
okFocus?: boolean;
|
||||
}>(), {
|
||||
isModified: false,
|
||||
size: "lg"
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
shown: [];
|
||||
hide: [];
|
||||
hidden: [];
|
||||
submit: [event: CustomSubmitEvent];
|
||||
}>();
|
||||
|
||||
const validatedFormRef = ref<InstanceType<typeof ValidatedForm>>();
|
||||
const isSubmitting = computed(() => validatedFormRef.value?.formData.isSubmitting);
|
||||
const submitRef = ref<HTMLElement>();
|
||||
|
||||
const modalRef = ref<HTMLElement>();
|
||||
const modal = useModal(modalRef, {
|
||||
emit,
|
||||
onShown: () => {
|
||||
if (props.okFocus) {
|
||||
submitRef.value?.focus();
|
||||
} else {
|
||||
modalRef.value?.querySelector<HTMLElement>("[autofocus],.fm-autofocus")?.focus();
|
||||
}
|
||||
|
||||
emit("shown");
|
||||
},
|
||||
onHide: () => {
|
||||
emit("hide");
|
||||
},
|
||||
onHidden: () => {
|
||||
emit("hidden");
|
||||
},
|
||||
static: computed(() => isSubmitting.value || props.isBusy || props.noCancel || props.isModified)
|
||||
});
|
||||
|
||||
const isCloseButton = computed(() => !props.isCreate && !props.isModified);
|
||||
|
||||
function handleSubmit(event: CustomSubmitEvent) {
|
||||
emit("submit", event);
|
||||
if (isCloseButton.value) {
|
||||
modal.hide();
|
||||
} else {
|
||||
emit("submit", event);
|
||||
}
|
||||
}
|
||||
|
||||
const expose = reactiveReadonlyView(() => ({
|
||||
|
@ -60,8 +92,13 @@
|
|||
:data-bs-backdrop="isSubmitting || props.isBusy || props.noCancel || props.isModified ? 'static' : 'true'"
|
||||
:data-bs-keyboard="isSubmitting || props.isBusy || props.noCancel || props.isModified ? 'false' : 'true'"
|
||||
>
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<ValidatedForm class="modal-content" @submit="handleSubmit" ref="validatedFormRef">
|
||||
<div class="modal-dialog modal-dialog-scrollable" :style="props.stackLevel ? { padding: `${20*props.stackLevel}px ${40*props.stackLevel}px` } : undefined">
|
||||
<ValidatedForm
|
||||
class="modal-content"
|
||||
@submit="handleSubmit"
|
||||
ref="validatedFormRef"
|
||||
:noValidate="isCloseButton"
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5">{{props.title}}</h1>
|
||||
<button
|
||||
|
@ -82,20 +119,20 @@
|
|||
<div style="flex-grow: 1"></div>
|
||||
|
||||
<button
|
||||
v-if="!props.noCancel"
|
||||
v-if="!props.noCancel && !isCloseButton"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:class="props.isModified || props.isCreate ? 'btn-secondary' : 'btn-primary'"
|
||||
@click="modal.hide()"
|
||||
:disabled="isSubmitting || props.isBusy"
|
||||
>{{props.isModified || props.isCreate ? 'Cancel' : 'Close'}}</button>
|
||||
>Cancel</button>
|
||||
|
||||
<button
|
||||
v-if="props.noCancel || props.isModified || props.isCreate"
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:class="props.okVariant && `btn-${props.okVariant}`"
|
||||
:disabled="isSubmitting || props.isBusy"
|
||||
>{{props.okLabel ?? 'Save'}}</button>
|
||||
ref="submitRef"
|
||||
>{{props.okLabel ?? (isCloseButton ? 'Close' : 'Save')}}</button>
|
||||
</div>
|
||||
</ValidatedForm>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { type StyleValue, computed, ref, watchEffect } from "vue";
|
||||
import { type StyleValue, computed, ref } from "vue";
|
||||
import HybridPopover from "./hybrid-popover.vue";
|
||||
import vValidity, { vValidityContext } from "./validated-form/validity";
|
||||
import { useDomEventListener, useNonClickFocusHandler, useNonDragClickHandler } from "../../utils/utils";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
id?: string;
|
||||
|
@ -20,6 +21,7 @@
|
|||
const emit = defineEmits<{
|
||||
keydown: [event: KeyboardEvent];
|
||||
"update:modelValue": [value: string];
|
||||
focusPopover: [];
|
||||
}>();
|
||||
|
||||
const value = computed({
|
||||
|
@ -29,49 +31,77 @@
|
|||
}
|
||||
});
|
||||
|
||||
const focusFilterOnNextOpen = ref(false);
|
||||
const isOpen = ref(false);
|
||||
|
||||
const containerRef = ref<HTMLElement>();
|
||||
const inputRef = ref<HTMLInputElement>();
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
useDomEventListener(document.body, "keydown", handleBodyKeyDownCapture, { capture: true });
|
||||
useDomEventListener(document.body, "keydown", handleBodyKeyDown, { capture: true });
|
||||
|
||||
function handleBodyKeyDownCapture(e: Event): void {
|
||||
if (isOpen.value) {
|
||||
document.body.addEventListener("keydown", handleBodyKeyDown);
|
||||
|
||||
onCleanup(() => {
|
||||
document.body.addEventListener("keydown", handleBodyKeyDown);
|
||||
});
|
||||
const event = e as KeyboardEvent;
|
||||
if (event.key == "Enter") {
|
||||
inputRef.value?.focus();
|
||||
isOpen.value = false;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else if (event.key == "Escape") {
|
||||
isOpen.value = false;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
inputRef.value?.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleBodyKeyDown(event: KeyboardEvent): void {
|
||||
if (event.key == "Enter") {
|
||||
inputRef.value?.focus();
|
||||
isOpen.value = false;
|
||||
event.preventDefault();
|
||||
} else if (["ArrowUp", "ArrowLeft", "ArrowDown", "ArrowRight"].includes(event.key)) {
|
||||
emit("keydown", event);
|
||||
} else if (event.key == "Escape" && isOpen.value) {
|
||||
isOpen.value = false;
|
||||
event.preventDefault();
|
||||
inputRef.value?.focus();
|
||||
function handleBodyKeyDown(e: Event): void {
|
||||
if (isOpen.value) {
|
||||
emit("keydown", e as KeyboardEvent);
|
||||
}
|
||||
}
|
||||
|
||||
function handleInputKeyDown(event: KeyboardEvent): void {
|
||||
if (["ArrowDown", "ArrowUp"].includes(event.key)) {
|
||||
if (!isOpen.value) {
|
||||
isOpen.value = true;
|
||||
event.preventDefault();
|
||||
} else
|
||||
emit("keydown", event);
|
||||
event.stopPropagation(); // To not be picked up by handleBodyKeyDown()
|
||||
if (["ArrowDown", "ArrowUp"].includes(event.key) && !isOpen.value) {
|
||||
focusFilterOnNextOpen.value = true;
|
||||
isOpen.value = true;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else if (event.key == "Escape" && isOpen.value) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // Prevent closing outer modal
|
||||
isOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
useNonClickFocusHandler(() => inputRef.value, open);
|
||||
useNonDragClickHandler(() => inputRef.value, open);
|
||||
|
||||
function open() {
|
||||
focusFilterOnNextOpen.value = true;
|
||||
isOpen.value = true;
|
||||
}
|
||||
|
||||
function handleInputDblClick() {
|
||||
focusFilterOnNextOpen.value = false;
|
||||
inputRef.value?.focus();
|
||||
}
|
||||
|
||||
function handleToggleButtonClick() {
|
||||
if (isOpen.value) {
|
||||
isOpen.value = false;
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
function handleShown() {
|
||||
if (focusFilterOnNextOpen.value) {
|
||||
emit("focusPopover");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -80,13 +110,16 @@
|
|||
v-model:show="isOpen"
|
||||
:enforceElementWidth="props.enforceElementWidth"
|
||||
:customClass="props.customClass"
|
||||
@shown="handleShown"
|
||||
ignoreClick
|
||||
>
|
||||
<template #trigger>
|
||||
<div class="input-group has-validation" v-validity-context>
|
||||
<span
|
||||
class="input-group-text"
|
||||
@click="inputRef?.focus()"
|
||||
:style="props.previewStyle">
|
||||
:style="props.previewStyle"
|
||||
>
|
||||
<slot name="preview"></slot>
|
||||
</span>
|
||||
<input
|
||||
|
@ -99,7 +132,15 @@
|
|||
:id="id"
|
||||
ref="inputRef"
|
||||
@keydown="handleInputKeyDown"
|
||||
@dblclick="handleInputDblClick"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary dropdown-toggle"
|
||||
:class="{ active: isOpen }"
|
||||
tabindex="-1"
|
||||
@click="handleToggleButtonClick()"
|
||||
></button>
|
||||
<div class="invalid-feedback">
|
||||
{{props.validationError}}
|
||||
</div>
|
||||
|
@ -110,35 +151,6 @@
|
|||
<slot :is-modal="isModal" :close="close"></slot>
|
||||
</template>
|
||||
</HybridPopover>
|
||||
|
||||
<!-- <b-popover
|
||||
:target="`${id}-input-group`"
|
||||
:container="body"
|
||||
triggers="manual"
|
||||
placement="bottom"
|
||||
:fallback-placement="[]"
|
||||
:custom-class="`fm-picker-popover ${uniqueClass} ${customClass}`"
|
||||
:delay="0"
|
||||
boundary="viewport"
|
||||
@show="handleOpenPopover"
|
||||
@hidden="handleClosePopover"
|
||||
:show.sync="popoverOpen"
|
||||
>
|
||||
<div @focusin.stop="handlePopoverFocus" class="fm-field-popover-content">
|
||||
<slot :is-modal="false" :close="close"></slot>
|
||||
</div>
|
||||
</b-popover>
|
||||
|
||||
<b-modal
|
||||
:id="`${id}-modal`"
|
||||
v-model="modalOpen"
|
||||
:body-class="`fm-picker-modal ${customClass}`"
|
||||
ok-only
|
||||
hide-header
|
||||
scrollable
|
||||
>
|
||||
<slot :is-modal="true" :close="close"></slot>
|
||||
</b-modal> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { computed, ref, toRef, watch, watchEffect } from "vue";
|
||||
import { Popover, Tooltip } from "bootstrap";
|
||||
import { useResizeObserver } from "../../utils/vue";
|
||||
import { useDomEventListener } from "../../utils/utils";
|
||||
|
||||
/**
|
||||
* Like Bootstrap Popover, but uses an existing popover element rather than creating a new one. This way, the popover
|
||||
|
@ -41,107 +42,91 @@
|
|||
hideOnOutsideClick?: boolean;
|
||||
/** If true, the width of the popover will be fixed to the width of the element. */
|
||||
enforceElementWidth?: boolean;
|
||||
placement?: Tooltip.PopoverPlacement
|
||||
placement?: Tooltip.PopoverPlacement;
|
||||
}>(), {
|
||||
placement: "bottom"
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:show": [show: boolean];
|
||||
shown: [];
|
||||
hide: [];
|
||||
hidden: [];
|
||||
}>();
|
||||
|
||||
const popoverContent = ref<HTMLElement | null>(null);
|
||||
const popoverContent = ref<HTMLElement>();
|
||||
const renderPopover = ref(false);
|
||||
|
||||
const show = async () => {
|
||||
renderPopover.value = true;
|
||||
await nextTick();
|
||||
if (props.element) {
|
||||
CustomPopover.getOrCreateInstance(props.element, {
|
||||
watchEffect((onCleanup) => {
|
||||
onCleanup(() => {}); // TODO: Delete me https://github.com/vuejs/core/issues/5151#issuecomment-1515613484
|
||||
|
||||
if (props.element && popoverContent.value) {
|
||||
const popover = new CustomPopover(props.element, {
|
||||
placement: props.placement,
|
||||
content: popoverContent.value!,
|
||||
content: popoverContent.value,
|
||||
trigger: 'manual',
|
||||
popperConfig: (defaultConfig) => ({
|
||||
...defaultConfig,
|
||||
strategy: "fixed"
|
||||
})
|
||||
}).show();
|
||||
}
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
if (props.element) {
|
||||
CustomPopover.getInstance(props.element)?.hide(); // Will be destroyed by hidden.bs.popover event listener
|
||||
}
|
||||
|
||||
if (props.show) {
|
||||
emit("update:show", false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHidden = () => {
|
||||
if (props.element) {
|
||||
CustomPopover.getInstance(props.element)?.dispose();
|
||||
}
|
||||
renderPopover.value = false;
|
||||
};
|
||||
|
||||
watch([
|
||||
() => props.element,
|
||||
() => props.placement
|
||||
], ([el], oldValues, onCleanup) => {
|
||||
if (el) {
|
||||
el.addEventListener("hidden.bs.popover", handleHidden);
|
||||
|
||||
if (props.show) {
|
||||
show();
|
||||
}
|
||||
});
|
||||
popover.show();
|
||||
|
||||
onCleanup(() => {
|
||||
el.removeEventListener("hidden.bs.popover", handleHidden);
|
||||
CustomPopover.getInstance(el)?.dispose();
|
||||
popover.dispose();
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => props.show, () => {
|
||||
if (props.element) {
|
||||
if (props.show) {
|
||||
show();
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const handleDocumentClick = (e: MouseEvent) => {
|
||||
if (props.hideOnOutsideClick && e.target instanceof Node && !props.element?.contains(e.target) && !popoverContent.value?.contains(e.target)) {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleDocumentClick, { capture: true });
|
||||
|
||||
// TODO: Handle element blur (and focus?)
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleDocumentClick, { capture: true });
|
||||
if (props.element) {
|
||||
CustomPopover.getInstance(props.element)?.dispose()
|
||||
useDomEventListener(toRef(() => props.element), "shown.bs.popover", () => {
|
||||
emit("shown");
|
||||
});
|
||||
|
||||
useDomEventListener(toRef(() => props.element), "hide.bs.popover", () => {
|
||||
emit("hide");
|
||||
});
|
||||
|
||||
useDomEventListener(toRef(() => props.element), "hidden.bs.popover", () => {
|
||||
renderPopover.value = false;
|
||||
emit("hidden");
|
||||
});
|
||||
|
||||
watch(() => props.show, (show) => {
|
||||
renderPopover.value = show;
|
||||
});
|
||||
|
||||
useDomEventListener(() => props.element, "focusout", (e: Event) => {
|
||||
const event = e as FocusEvent;
|
||||
// relatedTarget == null: target is out of viewport (ignore to allow focussing dev tools)
|
||||
if (event.relatedTarget && !popoverContent.value?.contains(event.relatedTarget as Node) && !props.element?.contains(event.relatedTarget as Node)) {
|
||||
emit("update:show", false);
|
||||
}
|
||||
});
|
||||
|
||||
function handlePopoverFocusOut(event: FocusEvent) {
|
||||
// relatedTarget == null: target is out of viewport (ignore to allow focussing dev tools)
|
||||
if (event.relatedTarget && !popoverContent.value?.contains(event.relatedTarget as Node) && !props.element?.contains(event.relatedTarget as Node)) {
|
||||
emit("update:show", false);
|
||||
}
|
||||
}
|
||||
|
||||
useDomEventListener(document, "click", (e: Event) => {
|
||||
if (props.show && props.hideOnOutsideClick && e.target instanceof Node && !props.element?.contains(e.target) && !popoverContent.value?.contains(e.target)) {
|
||||
emit("update:show", false);
|
||||
}
|
||||
}, { capture: true });
|
||||
|
||||
const elementSize = useResizeObserver(computed(() => props.enforceElementWidth ? props.element : undefined));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="renderPopover"
|
||||
class="popover fade bs-popover-auto"
|
||||
class="popover fm-popover fade bs-popover-auto"
|
||||
ref="popoverContent"
|
||||
:style="props.enforceElementWidth && elementSize ? { maxWidth: 'none', width: `${elementSize.contentRect.width}px` } : undefined"
|
||||
@focusout="handlePopoverFocusOut"
|
||||
:tabindex="-1 /* Allow focusing by click (for focusout event relatedTarget), do not allow focusing by tab */"
|
||||
>
|
||||
<div class="popover-arrow"></div>
|
||||
<h3 v-if="$slots.header" class="popover-header">
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
const props = defineProps<{
|
||||
value: string | undefined;
|
||||
items: Record<string, string>;
|
||||
/** If true, tabindex="-1" will be set on all elements. */
|
||||
noFocus?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -14,8 +16,8 @@
|
|||
const containerRef = ref<HTMLElement>();
|
||||
|
||||
const code = computed(() => Object.entries(props.items).map(([key, val]) => (`
|
||||
<li data-fm-item="${quoteHtml(key)}">
|
||||
<a href="javascript:" class="dropdown-item">
|
||||
<li class="list-group-item list-group-item-action" data-fm-item="${quoteHtml(key)}">
|
||||
<a href="javascript:"${props.noFocus ? ` tabindex="-1"` : ""}>
|
||||
${val}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -25,11 +27,13 @@
|
|||
if (containerRef.value) {
|
||||
for (const el of containerRef.value.querySelectorAll(".active")) {
|
||||
el.classList.remove("active");
|
||||
el.removeAttribute("aria-current");
|
||||
}
|
||||
|
||||
const active = props.value != null && [...containerRef.value.querySelectorAll<HTMLElement>("[data-fm-item]")].find((el) => el.getAttribute("data-fm-item") === props.value);
|
||||
if (active) {
|
||||
active.querySelector("a")!.classList.add("active");
|
||||
active.classList.add("active");
|
||||
active.setAttribute("aria-current", "true");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -46,5 +50,23 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<ul ref="containerRef" v-show="Object.keys(items).length > 0" v-html="code" @click="handleClick"></ul>
|
||||
</template>
|
||||
<ul
|
||||
ref="containerRef"
|
||||
v-show="Object.keys(items).length > 0"
|
||||
v-html="code"
|
||||
@click="handleClick"
|
||||
class="fm-prerendered-list list-group"
|
||||
></ul>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.fm-prerendered-list.fm-prerendered-list.fm-prerendered-list {
|
||||
border-radius: unset;
|
||||
|
||||
li {
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -48,12 +48,13 @@
|
|||
close();
|
||||
}
|
||||
|
||||
async function handleKeyDown(event: KeyboardEvent): Promise<void> {
|
||||
function handleKeyDown(event: KeyboardEvent): void {
|
||||
const newVal = arrowNavigation(Object.keys(items), props.modelValue, gridRef.value!.containerRef!, event);
|
||||
if (newVal) {
|
||||
emit("update:modelValue", newVal);
|
||||
await nextTick();
|
||||
gridRef.value?.containerRef?.querySelector<HTMLElement>(".active")?.focus();
|
||||
nextTick(() => {
|
||||
gridRef.value?.containerRef?.querySelector<HTMLElement>(".active > a")?.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -65,7 +66,6 @@
|
|||
customClass="fm-shape-field"
|
||||
:validationError="validationError"
|
||||
@keydown="handleKeyDown"
|
||||
enforceElementWidth
|
||||
>
|
||||
<template #preview>
|
||||
<span style="width: 1.4em"><img :src="valueSrc"></span>
|
||||
|
@ -79,6 +79,7 @@
|
|||
:value="modelValue ?? undefined"
|
||||
@click="handleClick($event, close)"
|
||||
ref="gridRef"
|
||||
noFocus
|
||||
></PrerenderedList>
|
||||
</template>
|
||||
</Picker>
|
||||
|
@ -86,6 +87,8 @@
|
|||
|
||||
<style lang="scss">
|
||||
.fm-shape-field {
|
||||
max-width: none;
|
||||
|
||||
.popover-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -100,7 +103,7 @@
|
|||
padding: 0;
|
||||
list-style-type: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 37px);
|
||||
grid-template-columns: repeat(5, 40px);
|
||||
overflow-y: auto;
|
||||
|
||||
li {
|
||||
|
@ -113,7 +116,7 @@
|
|||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
color: inherit;
|
||||
padding: 5px 8px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@
|
|||
});
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
onCleanup(() => {}); // TODO: Delete me https://github.com/vuejs/core/issues/5151#issuecomment-1515613484
|
||||
|
||||
if (innerSidebarRef.value) {
|
||||
const pan = new hammer.Manager(innerSidebarRef.value);
|
||||
pan.add(new hammer.Pan({ direction: hammer.DIRECTION_RIGHT }));
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
});
|
||||
|
||||
const filter = ref("");
|
||||
const filterRef = ref<HTMLElement>();
|
||||
|
||||
const items = computed(() => {
|
||||
const result: Record<string, string> = {};
|
||||
|
@ -51,7 +52,7 @@
|
|||
const validationError = computed(() => {
|
||||
if (props.validationError) {
|
||||
return props.validationError;
|
||||
} else if (props.modelValue && props.modelValue.length !== 1 && symbolList.includes(props.modelValue)) {
|
||||
} else if (props.modelValue && props.modelValue.length !== 1 && !symbolList.includes(props.modelValue)) {
|
||||
return "Unknown icon";
|
||||
} else {
|
||||
return undefined;
|
||||
|
@ -66,11 +67,16 @@
|
|||
async function handleKeyDown(event: KeyboardEvent): Promise<void> {
|
||||
const newVal = arrowNavigation(Object.keys(items.value), props.modelValue, gridRef.value!.containerRef!, event);
|
||||
if (newVal) {
|
||||
filterRef.value?.focus();
|
||||
emit("update:modelValue", newVal);
|
||||
await nextTick();
|
||||
gridRef.value?.containerRef?.querySelector<HTMLElement>(".active")?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusPopover() {
|
||||
filterRef.value?.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -81,6 +87,7 @@
|
|||
:validationError="validationError"
|
||||
@keydown="handleKeyDown"
|
||||
enforceElementWidth
|
||||
@focusPopover="handleFocusPopover()"
|
||||
>
|
||||
<template #preview>
|
||||
<Icon :icon="modelValue ?? undefined"></Icon>
|
||||
|
@ -89,11 +96,12 @@
|
|||
<template #default="{ close }">
|
||||
<input
|
||||
type="search"
|
||||
class="form-control"
|
||||
class="form-control fm-keyboard-navigation-exception"
|
||||
v-model="filter"
|
||||
placeholder="Filter"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
ref="filterRef"
|
||||
tabindex="-1"
|
||||
/>
|
||||
|
||||
<div v-if="Object.keys(items).length == 0" class="alert alert-danger mt-2 mb-1">No icons could be found.</div>
|
||||
|
@ -101,8 +109,9 @@
|
|||
<PrerenderedList
|
||||
:items="items"
|
||||
:value="modelValue ?? undefined"
|
||||
noFocus
|
||||
@click="handleClick($event, close)"
|
||||
ref="grid"
|
||||
ref="gridRef"
|
||||
></PrerenderedList>
|
||||
</template>
|
||||
</Picker>
|
|
@ -20,6 +20,7 @@
|
|||
variant?: ThemeColour;
|
||||
noCloseButton?: boolean;
|
||||
autoHide?: boolean;
|
||||
onHide?: () => void;
|
||||
onHidden?: () => void;
|
||||
}
|
||||
|
||||
|
@ -27,6 +28,7 @@
|
|||
onClick?: (e: MouseEvent) => void;
|
||||
label: string;
|
||||
href?: string;
|
||||
variant?: ThemeColour;
|
||||
}
|
||||
|
||||
interface ToastInstance extends ToastOptions {
|
||||
|
@ -121,6 +123,10 @@
|
|||
toastRef.addEventListener("shown.bs.toast", () => resolve());
|
||||
Toast.getOrCreateInstance(toastRef, { autohide: !!toast.autoHide }).show();
|
||||
|
||||
toastRef.addEventListener("hide.bs.toast", () => {
|
||||
toast.onHide?.();
|
||||
});
|
||||
|
||||
toastRef.addEventListener("hidden.bs.toast", () => {
|
||||
toasts.value = toasts.value.filter((t) => t !== toast);
|
||||
toastRefs.delete(toast);
|
||||
|
@ -173,19 +179,21 @@
|
|||
{{toast.message}}
|
||||
</div>
|
||||
|
||||
<div class="fm-toast-actions">
|
||||
<div v-if="(toast.actions?.length ?? 0) > 0" class="btn-toolbar mt-2 pt-2 border-top">
|
||||
<template v-for="(action, idx) in toast.actions" :key="idx">
|
||||
<button
|
||||
v-if="!action.href"
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
class="btn btn-sm"
|
||||
:class="`btn-${action.variant ?? 'secondary'}`"
|
||||
@click="action.onClick"
|
||||
>{{action.label}}</button>
|
||||
|
||||
<a
|
||||
v-if="action.href"
|
||||
:href="action.href"
|
||||
class="btn btn-secondary btn-sm"
|
||||
class="btn btn-sm"
|
||||
:class="`btn-${action.variant ?? 'secondary'}`"
|
||||
@click="action.onClick"
|
||||
>{{action.label}}</a>
|
||||
</template>
|
||||
|
@ -198,11 +206,5 @@
|
|||
<style lang="scss">
|
||||
.fm-toasts {
|
||||
z-index: 10002;
|
||||
|
||||
.fm-toast-actions {
|
||||
button + button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,13 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { toRef } from "vue";
|
||||
import type { ButtonSize } from "../../utils/bootstrap";
|
||||
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import type { RouteDestination } from "../facil-map-context-provider/map-context";
|
||||
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import type { RouteDestination } from "../facil-map-context-provider/route-form-tab-context";
|
||||
import DropdownMenu from "./dropdown-menu.vue";
|
||||
|
||||
const context = injectContextRequired();
|
||||
const mapContext = requireMapContext(context);
|
||||
const searchBoxContext = toRef(() => context.components.searchBox);
|
||||
const routeFormContext = toRef(() => context.components.routeFormTab);
|
||||
|
||||
const props = defineProps<{
|
||||
destination: RouteDestination;
|
||||
|
@ -15,15 +15,22 @@
|
|||
size?: ButtonSize;
|
||||
}>();
|
||||
|
||||
function useAs(event: "route-set-from" | "route-add-via" | "route-set-to"): void {
|
||||
mapContext.value.emit(event, props.destination);
|
||||
searchBoxContext.value!.activateTab(`fm${context.id}-route-form-tab`);
|
||||
function useAs(type: "from" | "via" | "to"): void {
|
||||
if (type === "from") {
|
||||
routeFormContext.value!.setFrom(props.destination);
|
||||
} else if (type === "via") {
|
||||
routeFormContext.value!.addVia(props.destination);
|
||||
} else if (type === "to") {
|
||||
routeFormContext.value!.setTo(props.destination);
|
||||
}
|
||||
|
||||
searchBoxContext.value!.activateTab(`fm${context.id}-route-form-tab`, { autofocus: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu
|
||||
v-if="searchBoxContext && context.settings.search"
|
||||
v-if="searchBoxContext && routeFormContext && context.settings.search"
|
||||
:size="props.size"
|
||||
:isDisabled="props.isDisabled"
|
||||
label="Use as"
|
||||
|
@ -32,7 +39,7 @@
|
|||
<a
|
||||
href="javascript:"
|
||||
class="dropdown-item"
|
||||
@click="useAs('route-set-from')"
|
||||
@click="useAs('from')"
|
||||
>Route start</a>
|
||||
</li>
|
||||
|
||||
|
@ -40,7 +47,7 @@
|
|||
<a
|
||||
href="javascript:"
|
||||
class="dropdown-item"
|
||||
@click="useAs('route-add-via')"
|
||||
@click="useAs('via')"
|
||||
>Route via</a>
|
||||
</li>
|
||||
|
||||
|
@ -48,7 +55,7 @@
|
|||
<a
|
||||
href="javascript:"
|
||||
class="dropdown-item"
|
||||
@click="useAs('route-set-to')"
|
||||
@click="useAs('to')"
|
||||
>Route destination</a>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { type Ref, onScopeDispose, reactive, readonly, ref, watchEffect } from "vue";
|
||||
import { type Ref, onScopeDispose, reactive, readonly, ref, watchEffect, toRef } from "vue";
|
||||
import { type ExtendableEventMixin, extendableEventMixin } from "../../../utils/utils";
|
||||
import { useToasts } from "../toasts/toasts.vue";
|
||||
|
||||
|
@ -16,7 +16,11 @@
|
|||
|
||||
const allForms = reactive(new Map<Ref<HTMLFormElement | undefined>, ValidatedFormData>());
|
||||
|
||||
export function useValidatedForm(formRef: Ref<HTMLFormElement | undefined>, onSubmit: (event: CustomSubmitEvent) => void): Readonly<ValidatedFormData> {
|
||||
export function useValidatedForm(
|
||||
formRef: Ref<HTMLFormElement | undefined>,
|
||||
onSubmit: (event: CustomSubmitEvent) => void,
|
||||
{ noValidate }: { noValidate?: Ref<boolean> } = {}
|
||||
): Readonly<ValidatedFormData> {
|
||||
const toasts = useToasts();
|
||||
const validationPromises = new Map<Element, Promise<any>>();
|
||||
const isValidating = reactive(new Map<Element, boolean>());
|
||||
|
@ -29,16 +33,20 @@
|
|||
data.isTouched = true;
|
||||
data.isSubmitting = true;
|
||||
|
||||
await Promise.all(validationPromises.values());
|
||||
try {
|
||||
if (!noValidate?.value) {
|
||||
await Promise.all(validationPromises.values());
|
||||
|
||||
if (formRef.value!.checkValidity()) {
|
||||
try {
|
||||
const event = { ...extendableEventMixin };
|
||||
onSubmit(event);
|
||||
await event._awaitPromises();
|
||||
} finally {
|
||||
data.isSubmitting = false;
|
||||
if (!formRef.value!.checkValidity()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const event = { ...extendableEventMixin };
|
||||
onSubmit(event);
|
||||
await event._awaitPromises();
|
||||
} finally {
|
||||
data.isSubmitting = false;
|
||||
}
|
||||
}),
|
||||
setValidationPromise: (element, promise) => {
|
||||
|
@ -83,6 +91,7 @@
|
|||
const props = defineProps<{
|
||||
action?: string;
|
||||
target?: string;
|
||||
noValidate?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -92,6 +101,8 @@
|
|||
const formRef = ref<HTMLFormElement>();
|
||||
const formData = useValidatedForm(formRef, (event) => {
|
||||
emit("submit", event);
|
||||
}, {
|
||||
noValidate: toRef(() => props.noValidate)
|
||||
});
|
||||
|
||||
defineExpose({ formData });
|
||||
|
@ -102,7 +113,6 @@
|
|||
@submit.prevent="formData.submit()"
|
||||
novalidate
|
||||
ref="formRef"
|
||||
:class="{ 'was-validated': formData.isTouched }"
|
||||
:action="props.action"
|
||||
:target="props.target ?? 'javascript:'"
|
||||
>
|
||||
|
|
|
@ -69,8 +69,12 @@ const updateValidityContext: Directive<FormElement, void> = (el, binding) => {
|
|||
el.classList.add("was-validated");
|
||||
};
|
||||
el.addEventListener("input", el._fmValidityInputListener);
|
||||
el.addEventListener("blur", el._fmValidityInputListener);
|
||||
el.addEventListener("focusout", el._fmValidityInputListener);
|
||||
}
|
||||
|
||||
el.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>("input,textarea,select")?.form?.addEventListener("submit", el._fmValidityInputListener);
|
||||
|
||||
if (el._fmValidityTouched) {
|
||||
el.classList.add("was-validated");
|
||||
}
|
||||
|
@ -78,10 +82,18 @@ const updateValidityContext: Directive<FormElement, void> = (el, binding) => {
|
|||
|
||||
export const vValidityContext: Directive<FormElement, void> = {
|
||||
mounted: updateValidityContext,
|
||||
beforeUpdate: (el) => {
|
||||
if (el._fmValidityInputListener) {
|
||||
el.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>("input,textarea,select")?.removeEventListener("submit", el._fmValidityInputListener); // Added again during updated
|
||||
}
|
||||
},
|
||||
updated: updateValidityContext,
|
||||
beforeUnmount: (el) => {
|
||||
if (el._fmValidityInputListener) {
|
||||
el.removeEventListener("input", el._fmValidityInputListener);
|
||||
el.removeEventListener("blur", el._fmValidityInputListener);
|
||||
el.removeEventListener("focusout", el._fmValidityInputListener);
|
||||
el.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>("input,textarea,select")?.removeEventListener("submit", el._fmValidityInputListener);
|
||||
delete el._fmValidityInputListener;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ export { default as SearchForm } from "./components/search-form/search-form.vue"
|
|||
export { default as SearchResultInfo } from "./components/search-result-info.vue";
|
||||
export { default as SearchResults } from "./components/search-results/search-results.vue";
|
||||
export { default as Toolbox } from "./components/toolbox/toolbox.vue";
|
||||
export { default as ColourField } from "./components/ui/colour-field.vue";
|
||||
export { default as ColourPicker } from "./components/ui/colour-picker.vue";
|
||||
export { default as ElevationPlot } from "./components/ui/elevation-plot.vue";
|
||||
export { default as ElevationStats } from "./components/ui/elevation-stats.vue";
|
||||
export { default as FieldInput } from "./components/ui/field-input.vue";
|
||||
|
@ -63,10 +63,10 @@ export { default as Icon } from "./components/ui/icon.vue";
|
|||
export { default as Picker } from "./components/ui/picker.vue";
|
||||
export { default as PrerenderedList } from "./components/ui/prerendered-list.vue";
|
||||
export { default as RouteMode } from "./components/ui/route-mode.vue";
|
||||
export { default as ShapeField } from "./components/ui/shape-field.vue";
|
||||
export { default as ShapePicker } from "./components/ui/shape-picker.vue";
|
||||
export { default as Sidebar } from "./components/ui/sidebar.vue";
|
||||
export { default as SizeField } from "./components/ui/size-field.vue";
|
||||
export { default as SymbolField } from "./components/ui/symbol-field.vue";
|
||||
export { default as SizePicker } from "./components/ui/size-picker.vue";
|
||||
export { default as SymbolPicker } from "./components/ui/symbol-picker.vue";
|
||||
export { default as Toast } from "./components/ui/toasts/toast.vue";
|
||||
export * from "./components/ui/toasts/toasts.vue";
|
||||
export { default as WidthField } from "./components/ui/width-field.vue";
|
||||
export { default as WidthPicker } from "./components/ui/width-picker.vue";
|
|
@ -27,4 +27,19 @@
|
|||
.fm-form-range-with-label {
|
||||
// Same padding-top as .col-form-label plus half line-height (1.5) minus half range input height (16px)
|
||||
padding-top: calc(0.375rem + var(--bs-border-width) + 0.75rem - 8px);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make a carousel use flex containers rather than floating block containers.
|
||||
*/
|
||||
.fm-flex-carousel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
|
||||
> .carousel-item.active, > .carousel-item-next, > .carousel-item-prev {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ export function useCarousel(element: Ref<HTMLElement | undefined>): Readonly<Car
|
|||
});
|
||||
|
||||
watch(element, (newRef, oldRef, onCleanup) => {
|
||||
onCleanup(() => {}); // TODO: Delete me https://github.com/vuejs/core/issues/5151#issuecomment-1515613484
|
||||
|
||||
if (newRef) {
|
||||
const carousel = new Carousel(newRef, {
|
||||
interval: 0,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { addClickListener } from "facilmap-leaflet";
|
||||
import type { ID, Type } from "facilmap-types";
|
||||
import { getUniqueId } from "./utils";
|
||||
import type { ToastContext } from "../components/ui/toasts/toasts.vue";
|
||||
import type { FacilMapContext } from "../components/facil-map-context-provider/facil-map-context";
|
||||
import { requireClientContext, requireMapContext } from "../components/facil-map-context-provider/facil-map-context-provider.vue";
|
||||
|
@ -20,19 +19,29 @@ export function drawMarker(type: Type, context: FacilMapContext, toasts: ToastCo
|
|||
|
||||
mapContext.value.components.selectionHandler.setSelectedItems([{ type: "marker", id: marker.id }], true);
|
||||
|
||||
if (!mapContext.value.components.map.fmFilterFunc(marker, client.value.types[marker.typeId]))
|
||||
toasts.showToast(getUniqueId("fm-draw-add-marker"), `${type.name} successfully added`, "The marker was successfully added, but the active filter is preventing it from being shown.", { variant: "success", noCloseButton: false });
|
||||
if (!mapContext.value.components.map.fmFilterFunc(marker, client.value.types[marker.typeId])) {
|
||||
toasts.showToast(
|
||||
undefined,
|
||||
`${type.name} successfully added`,
|
||||
"The marker was successfully added, but the active filter is preventing it from being shown.",
|
||||
{ variant: "success", autoHide: true }
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
toasts.showErrorToast("fm-draw-add-marker", "Error adding marker", err);
|
||||
}
|
||||
});
|
||||
|
||||
toasts.showToast("fm-draw-add-marker", `Add ${type.name}`, "Please click on the map to add a marker.", {
|
||||
noCloseButton: true,
|
||||
actions: [
|
||||
{ label: "Cancel", onClick: () => {
|
||||
toasts.hideToast("fm-draw-add-marker");
|
||||
clickListener.cancel();
|
||||
} }
|
||||
{
|
||||
label: "Cancel",
|
||||
onClick: () => {
|
||||
toasts.hideToast("fm-draw-add-marker");
|
||||
clickListener.cancel();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
@ -69,13 +78,21 @@ export function moveMarker(markerId: ID, context: FacilMapContext, toasts: Toast
|
|||
}
|
||||
|
||||
toasts.showToast("fm-draw-drag-marker", "Drag marker", "Drag the marker to reposition it.", {
|
||||
noCloseButton: true,
|
||||
actions: [
|
||||
{ label: "Save", onClick: () => {
|
||||
finish(true);
|
||||
}},
|
||||
{ label: "Cancel", onClick: () => {
|
||||
finish(false);
|
||||
} }
|
||||
{
|
||||
label: "Save",
|
||||
variant: "primary",
|
||||
onClick: () => {
|
||||
finish(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
onClick: () => {
|
||||
finish(false);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
|
@ -92,13 +109,21 @@ export async function drawLine(type: Type, context: FacilMapContext, toasts: Toa
|
|||
const lineTemplate = await client.value.getLineTemplate({ typeId: type.id });
|
||||
|
||||
toasts.showToast("fm-draw-add-line", `Add ${type.name}`, "Click on the map to draw a line. Click “Finish” to save it.", {
|
||||
noCloseButton: true,
|
||||
actions: [
|
||||
{ label: "Finish", onClick: () => {
|
||||
mapContext.value.components.linesLayer.endDrawLine(true);
|
||||
}},
|
||||
{ label: "Cancel", onClick: () => {
|
||||
mapContext.value.components.linesLayer.endDrawLine(false);
|
||||
} }
|
||||
{
|
||||
label: "Finish",
|
||||
variant: "primary",
|
||||
onClick: () => {
|
||||
mapContext.value.components.linesLayer.endDrawLine(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
onClick: () => {
|
||||
mapContext.value.components.linesLayer.endDrawLine(false);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
|
@ -110,8 +135,14 @@ export async function drawLine(type: Type, context: FacilMapContext, toasts: Toa
|
|||
const line = await client.value.addLine({ typeId: type.id, routePoints });
|
||||
mapContext.value.components.selectionHandler.setSelectedItems([{ type: "line", id: line.id }], true);
|
||||
|
||||
if (!mapContext.value.components.map.fmFilterFunc(line, client.value.types[line.typeId]))
|
||||
toasts.showToast(getUniqueId("fm-draw-add-line"), `${type.name} successfully added`, "The line was successfully added, but the active filter is preventing it from being shown.", { variant: "success", noCloseButton: false });
|
||||
if (!mapContext.value.components.map.fmFilterFunc(line, client.value.types[line.typeId])) {
|
||||
toasts.showToast(
|
||||
undefined,
|
||||
`${type.name} successfully added`,
|
||||
"The line was successfully added, but the active filter is preventing it from being shown.",
|
||||
{ variant: "success", autoHide: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toasts.showErrorToast("fm-draw-add-line", "Error adding line", err);
|
||||
|
|
|
@ -2,13 +2,6 @@ import { Modal } from "bootstrap";
|
|||
import { type Ref, shallowRef, watch, watchEffect } from "vue";
|
||||
|
||||
export interface ModalConfig {
|
||||
emit?: {
|
||||
/**
|
||||
* Emitted when the modal is closed and the fade-out animation has finished. Should cause the parent component to remove the
|
||||
* modal from the tree.
|
||||
*/
|
||||
(type: 'hidden'): void;
|
||||
};
|
||||
/** Will be called when the fade-in animation has finished. */
|
||||
onShown?: (event: Modal.Event) => void;
|
||||
/** Will be called before the fade-out animation when the modal is closed. */
|
||||
|
@ -26,7 +19,7 @@ export interface ModalActions {
|
|||
/**
|
||||
* Enables a Bootstrap modal dialog on the element that is saved in the returned {@link ModalActions#ref}.
|
||||
*/
|
||||
export function useModal(modalRef: Ref<HTMLElement | undefined>, { emit, onShown, onHide, static: isStatic }: ModalConfig): ModalActions {
|
||||
export function useModal(modalRef: Ref<HTMLElement | undefined>, { onShown, onHide, onHidden, static: isStatic }: ModalConfig): ModalActions {
|
||||
const modal = shallowRef<Modal>();
|
||||
|
||||
const handleShow = (e: Event) => {
|
||||
|
@ -46,12 +39,12 @@ export function useModal(modalRef: Ref<HTMLElement | undefined>, { emit, onShown
|
|||
};
|
||||
|
||||
const handleHidden = (e: Event) => {
|
||||
if (emit) {
|
||||
emit('hidden');
|
||||
}
|
||||
onHidden?.(e as Modal.Event);
|
||||
};
|
||||
|
||||
watch(modalRef, (newRef, oldRef, onCleanup) => {
|
||||
onCleanup(() => {}); // TODO: Delete me https://github.com/vuejs/core/issues/5151#issuecomment-1515613484
|
||||
|
||||
if (newRef) {
|
||||
modal.value = new Modal(newRef);
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ export function getGridColumnsCount(grid: Element): number {
|
|||
export function arrowNavigation<V>(values: V[], value: V | undefined, grid: Element, event: KeyboardEvent): V | undefined {
|
||||
if (!["ArrowUp", "ArrowLeft", "ArrowDown", "ArrowRight"].includes(event.key) || event.shiftKey || event.ctrlKey || event.metaKey)
|
||||
return;
|
||||
if (["ArrowLeft", "ArrowRight"].includes(event.key) && (event.target as HTMLElement | undefined)?.closest("input"))
|
||||
if (["ArrowLeft", "ArrowRight"].includes(event.key) && (event.target as HTMLElement | undefined)?.closest("input:not(.fm-keyboard-navigation-exception)"))
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import type { CRU, Field, Line, Marker, Type } from "facilmap-types";
|
||||
import type { Emitter } from "mitt";
|
||||
import { type DeepReadonly, type Ref, onBeforeUnmount, onMounted, watchEffect } from "vue";
|
||||
import { type DeepReadonly, type Ref, watchEffect, toRef, effectScope } from "vue";
|
||||
|
||||
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
// https://stackoverflow.com/a/62085569/242365
|
||||
export type DistributedKeyOf<T> = T extends any ? keyof T : never;
|
||||
|
||||
export type AnyRef<T> = T | Ref<T> | (() => T);
|
||||
|
||||
/**
|
||||
* Performs a 3-way merge. Takes the difference between oldObject and newObject and applies it to targetObject.
|
||||
* @param oldObject {Object}
|
||||
|
@ -60,10 +62,14 @@ export function isPromise(object: any): object is Promise<unknown> {
|
|||
return typeof object === 'object' && 'then' in object && typeof object.then === 'function';
|
||||
}
|
||||
|
||||
export function useEventListener<EventMap extends Record<string, unknown>, EventType extends keyof EventMap>(emitter: Ref<Emitter<EventMap> | DeepReadonly<Emitter<EventMap>> | undefined>, type: EventType, listener: (data: EventMap[EventType]) => void): void {
|
||||
export function useEventListener<EventMap extends Record<string, unknown>, EventType extends keyof EventMap>(emitter: AnyRef<Emitter<EventMap> | DeepReadonly<Emitter<EventMap>> | undefined>, type: EventType, listener: (data: EventMap[EventType]) => void): void {
|
||||
const emitterRef = toRef(emitter);
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
if (emitter.value) {
|
||||
const val = emitter.value;
|
||||
onCleanup(() => {}); // TODO: Delete me https://github.com/vuejs/core/issues/5151#issuecomment-1515613484
|
||||
|
||||
if (emitterRef.value) {
|
||||
const val = emitterRef.value;
|
||||
val.on(type, listener);
|
||||
onCleanup(() => {
|
||||
val.off(type, listener);
|
||||
|
@ -72,13 +78,18 @@ export function useEventListener<EventMap extends Record<string, unknown>, Event
|
|||
});
|
||||
}
|
||||
|
||||
export function useDomEventListener<Element extends EventTarget, Args extends Parameters<Element["addEventListener"]>>(element: EventTarget, ...args: Args): void {
|
||||
onMounted(() => {
|
||||
(element as any).addEventListener(...args);
|
||||
});
|
||||
export function useDomEventListener<Element extends EventTarget, Args extends Parameters<Element["addEventListener"]>>(element: AnyRef<EventTarget | undefined>, ...args: Args): void {
|
||||
watchEffect((onCleanup) => {
|
||||
onCleanup(() => {}); // TODO: Delete me https://github.com/vuejs/core/issues/5151#issuecomment-1515613484
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
(element as any).removeEventListener(...args);
|
||||
const elementRef = toRef(element);
|
||||
if (elementRef.value) {
|
||||
const el = elementRef.value as any;
|
||||
el.addEventListener(...args);
|
||||
onCleanup(() => {
|
||||
el.removeEventListener(...args);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -131,3 +142,63 @@ export function validateRequired(val: any): string | undefined {
|
|||
return "Must not be empty.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a focus handler on the given element that does not fire when the focus was given through a click.
|
||||
*/
|
||||
export function useNonClickFocusHandler(element: AnyRef<HTMLElement | undefined>, onFocus: (e: FocusEvent) => void): void {
|
||||
let lastEvent: {
|
||||
timeout: ReturnType<typeof setTimeout>;
|
||||
hadMouseDown?: boolean;
|
||||
focusEvent?: FocusEvent;
|
||||
} | undefined;
|
||||
|
||||
useDomEventListener(element, "mousedown", () => {
|
||||
lastEvent = {
|
||||
...lastEvent,
|
||||
timeout: lastEvent?.timeout ?? setTimeout(handleTimeout, 0),
|
||||
hadMouseDown: true
|
||||
};
|
||||
});
|
||||
|
||||
useDomEventListener(element, "focus", (e: Event) => {
|
||||
lastEvent = {
|
||||
...lastEvent,
|
||||
timeout: lastEvent?.timeout ?? setTimeout(handleTimeout, 0),
|
||||
focusEvent: e as FocusEvent
|
||||
};
|
||||
});
|
||||
|
||||
function handleTimeout() {
|
||||
if (lastEvent?.focusEvent && !lastEvent.hadMouseDown) {
|
||||
onFocus(lastEvent.focusEvent);
|
||||
}
|
||||
lastEvent = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a click handler on the given element that does not fire when the click is caused by a drag.
|
||||
*/
|
||||
export function useNonDragClickHandler(element: AnyRef<HTMLElement | undefined>, onClick: (e: MouseEvent) => void): void {
|
||||
let hasMoved = false;
|
||||
|
||||
useDomEventListener(element, "mousedown", () => {
|
||||
hasMoved = false;
|
||||
const scope = effectScope();
|
||||
scope.run(() => {
|
||||
useDomEventListener(document, "mousemove", () => {
|
||||
hasMoved = true;
|
||||
}, { capture: true });
|
||||
useDomEventListener(document, "mouseup", () => {
|
||||
scope.stop();
|
||||
}, { capture: true });
|
||||
});
|
||||
});
|
||||
|
||||
useDomEventListener(element, "click", (e) => {
|
||||
if (!hasMoved) {
|
||||
onClick(e as MouseEvent);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -59,6 +59,8 @@ export function useResizeObserver(
|
|||
});
|
||||
|
||||
watch(element, (value, oldValue, onCleanup) => {
|
||||
onCleanup(() => {}); // TODO: Delete me https://github.com/vuejs/core/issues/5151#issuecomment-1515613484
|
||||
|
||||
if (value) {
|
||||
observer.observe(value);
|
||||
onCleanup(() => {
|
||||
|
|
|
@ -138,10 +138,11 @@ export async function openSpecialQuery(query: string, context: FacilMapContext,
|
|||
const mapContext = requireMapContext(context);
|
||||
const client = requireClientContext(context);
|
||||
const searchBoxContext = toRef(() => context.components.searchBox);
|
||||
const routeFormTabContext = toRef(() => context.components.routeFormTab);
|
||||
|
||||
if(searchBoxContext.value && query.match(/ to /i)) {
|
||||
mapContext.value.emit("route-set-query", { query, zoom, smooth });
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-route-form-tab`);
|
||||
if(searchBoxContext.value && routeFormTabContext.value && query.match(/ to /i)) {
|
||||
routeFormTabContext.value.setQuery(query, zoom, smooth);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-route-form-tab`, { autofocus: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -55,29 +55,29 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@types/cheerio": "^0.22.32",
|
||||
"@types/geojson": "^7946.0.11",
|
||||
"@types/highland": "^2.12.16",
|
||||
"@types/leaflet": "^1.9.6",
|
||||
"@types/leaflet.markercluster": "^1.5.2",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/node-fetch": "^2.6.6",
|
||||
"@types/rollup-plugin-auto-external": "^2.0.3",
|
||||
"@types/yauzl": "^2.10.1",
|
||||
"@types/cheerio": "^0.22.34",
|
||||
"@types/geojson": "^7946.0.13",
|
||||
"@types/highland": "^2.12.18",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/leaflet.markercluster": "^1.5.4",
|
||||
"@types/lodash-es": "^4.17.11",
|
||||
"@types/node-fetch": "^2.6.9",
|
||||
"@types/rollup-plugin-auto-external": "^2.0.5",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"fast-glob": "^3.3.1",
|
||||
"happy-dom": "^12.9.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"happy-dom": "^12.10.3",
|
||||
"highland": "^2.13.5",
|
||||
"node-fetch": "^3.3.2",
|
||||
"rimraf": "^5.0.5",
|
||||
"rollup": "3",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"svgo": "^3.0.2",
|
||||
"svgo": "^3.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.11",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-css-injected-by-js": "^3.3.0",
|
||||
"vite-plugin-dts": "^3.6.0",
|
||||
"vite-plugin-dts": "^3.6.3",
|
||||
"vitest": "^0.34.6",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
"build": "yarn workspaces foreach -vt run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"eslint": "^8.50.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-vue": "^9.18.1"
|
||||
},
|
||||
"version": "0.0.0",
|
||||
|
|
|
@ -54,38 +54,38 @@
|
|||
"facilmap-utils": "workspace:^",
|
||||
"find-cache-dir": "^5.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"maxmind": "^4.3.15",
|
||||
"maxmind": "^4.3.17",
|
||||
"md5-file": "^5.0.0",
|
||||
"mysql2": "^3.6.1",
|
||||
"node-cron": "^3.0.2",
|
||||
"mysql2": "^3.6.3",
|
||||
"node-cron": "^3.0.3",
|
||||
"p-throttle": "^5.1.0",
|
||||
"sequelize": "^6.33.0",
|
||||
"sequelize": "^6.34.0",
|
||||
"socket.io": "^4.7.2",
|
||||
"string-similarity": "^4.0.4",
|
||||
"strip-bom-buf": "^3.0.1",
|
||||
"strip-bom-buf": "^4.0.0",
|
||||
"unzipper": "^0.10.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@digitak/esrun": "^3.2.25",
|
||||
"@types/cheerio": "^0.22.32",
|
||||
"@types/compression": "^1.7.3",
|
||||
"@types/debug": "^4.1.9",
|
||||
"@types/ejs": "^3.1.3",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/express-domain-middleware": "^0.0.7",
|
||||
"@types/geojson": "^7946.0.11",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/node": "^20.8.2",
|
||||
"@types/node-cron": "^3.0.9",
|
||||
"@types/string-similarity": "^4.0.0",
|
||||
"@types/cheerio": "^0.22.34",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-domain-middleware": "^0.0.9",
|
||||
"@types/geojson": "^7946.0.13",
|
||||
"@types/lodash-es": "^4.17.11",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/string-similarity": "^4.0.2",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"rimraf": "^5.0.5",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-dts": "^3.6.0",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-dts": "^3.6.3",
|
||||
"vitest": "^0.34.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -465,25 +465,25 @@ function _formatAddress(result: NominatimResult) {
|
|||
}
|
||||
|
||||
async function _loadUrl(url: string, completeOsmObjects = false): Promise<string> {
|
||||
let bodyBuf = await fetch(
|
||||
let bodyBuf = new Uint8Array(await fetch(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": config.userAgent
|
||||
}
|
||||
}
|
||||
).then(async (res) => Buffer.from(await res.arrayBuffer()));
|
||||
).then<ArrayBuffer>(async (res) => await res.arrayBuffer()));
|
||||
|
||||
if(!bodyBuf)
|
||||
throw new Error("Invalid response from server.");
|
||||
|
||||
if(bodyBuf[0] == 0x42 && bodyBuf[1] == 0x5a && bodyBuf[2] == 0x68) {// bzip2
|
||||
bodyBuf = Buffer.from(compressjs.Bzip2.decompressFile(bodyBuf));
|
||||
bodyBuf = Buffer.from(compressjs.Bzip2.decompressFile(Buffer.from(bodyBuf)));
|
||||
}
|
||||
else if(bodyBuf[0] == 0x1f && bodyBuf[1] == 0x8b && bodyBuf[2] == 0x08) // gzip
|
||||
bodyBuf = await util.promisify(zlib.gunzip.bind(zlib))(bodyBuf);
|
||||
|
||||
const body = stripBomBuf(bodyBuf).toString();
|
||||
const body = new TextDecoder().decode(stripBomBuf(bodyBuf));
|
||||
|
||||
if(url.match(/^https?:\/\/www\.freietonne\.de\/seekarte\/getOpenLayerPois\.php\?/))
|
||||
return body;
|
||||
|
|
|
@ -28,14 +28,14 @@
|
|||
"tsconfig.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/geojson": "^7946.0.11",
|
||||
"@types/geojson": "^7946.0.13",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^5.0.5",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-dts": "^3.6.0"
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-dts": "^3.6.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,22 +37,22 @@
|
|||
"filtrex": "^2.2.3",
|
||||
"jquery": "^3.7.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"linkify-string": "^4.1.1",
|
||||
"linkifyjs": "^4.1.1",
|
||||
"linkify-string": "^4.1.2",
|
||||
"linkifyjs": "^4.1.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^9.1.0"
|
||||
"marked": "^9.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cheerio": "^0.22.32",
|
||||
"@types/dompurify": "^3.0.3",
|
||||
"@types/jquery": "^3.5.21",
|
||||
"@types/jsdom": "^21.1.3",
|
||||
"@types/linkifyjs": "^2.1.5",
|
||||
"@types/cheerio": "^0.22.34",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/jquery": "^3.5.27",
|
||||
"@types/jsdom": "^21.1.5",
|
||||
"@types/linkifyjs": "^2.1.7",
|
||||
"rimraf": "^5.0.5",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-dts": "^3.6.0",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-dts": "^3.6.3",
|
||||
"vitest": "^0.34.6"
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue