facilmap/frontend/src/lib/components/overpass-form/overpass-form.vue

240 wiersze
6.5 KiB
Vue

<script setup lang="ts">
import { getAllOverpassPresets, getOverpassPreset, validateOverpassQuery } from "facilmap-leaflet";
import { computed, ref, watch } from "vue";
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import ValidatedField from "../ui/validated-form/validated-field.vue";
import { T, useI18n } from "../../utils/i18n";
import { sortBy } from "lodash-es";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const i18n = useI18n();
const activeTab = ref(0);
const searchTerm = ref("");
const customQuery = ref("");
const categories = computed(() => {
return getAllOverpassPresets().map((cat) => {
const presets = cat.presets.map((presets) => sortBy(presets.map((preset) => ({
...preset,
isChecked: mapContext.value.overpassPresets.some((p) => p.key === preset.key)
})), (preset) => preset.label.toLowerCase()));
return {
...cat,
presets,
checked: presets.flat().filter((preset) => preset.isChecked).length
}
});
});
const filteredPresets = computed(() => {
if (!searchTerm.value)
return [];
const lowerTerm = searchTerm.value.toLowerCase();
return categories.value.map((cat) => cat.presets).flat().flat().filter((preset) => preset.label.toLowerCase().includes(lowerTerm));
});
watch(() => mapContext.value.overpassCustom, () => {
customQuery.value = mapContext.value.overpassCustom;
}, { immediate: true });
function togglePreset(key: string, enable: boolean): void {
const without = mapContext.value.overpassPresets.filter((p) => p.key != key);
mapContext.value.components.overpassLayer.setQuery([
...without,
...(enable ? [getOverpassPreset(key)!] : [])
]);
}
function toggleIsCustom(): void {
if (mapContext.value.overpassIsCustom) {
mapContext.value.components.overpassLayer.setQuery(mapContext.value.overpassPresets);
} else {
mapContext.value.components.overpassLayer.setQuery(mapContext.value.overpassCustom);
}
}
async function validateCustomQuery(query: string, signal: AbortSignal): Promise<string | undefined> {
if (query) {
const result = await validateOverpassQuery(query, signal);
if (result) {
return result;
}
}
if (!signal.aborted) {
mapContext.value.components.overpassLayer.setQuery(query);
}
}
</script>
<template>
<div class="fm-overpass-form">
<template v-if="!mapContext.overpassIsCustom">
<input
class="form-control fm-autofocus"
type="search"
v-model="searchTerm"
:placeholder="i18n.t('overpass-form.filter')"
/>
<hr />
<template v-if="searchTerm">
<div class="checkbox-grid fm-search-box-collapse-point">
<template v-for="preset in filteredPresets" :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>
<template v-else>
<ul class="nav nav-pills">
<template v-for="(category, idx) in categories" :key="idx">
<li class="nav-item">
<a
href="javascript:"
class="nav-link"
:class="{ active: activeTab === idx }"
@click="activeTab = idx"
>
{{category.label}}
<span
v-if="category.checked > 0"
class="badge"
:class="activeTab == idx ? 'text-bg-secondary' : 'text-bg-primary'"
>{{category.checked}}</span>
</a>
</li>
</template>
</ul>
<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>
<ValidatedField
tag="form"
action="javascript:"
:value="customQuery"
:validators="[validateCustomQuery]"
:reportValid="!!customQuery"
immediate
:debounceMs="500"
>
<template #default="slotProps">
<textarea
v-model="customQuery"
rows="5"
class="form-control text-monospace"
:ref="slotProps.inputRef"
></textarea>
<div class="invalid-feedback" v-if="slotProps.validationError">
<pre>{{slotProps.validationError}}</pre>
</div>
</template>
</ValidatedField>
<hr />
<p>
<T k="overpass-form.custom-explanation-1">
<template #statement>
<a :href="i18n.t('overpass-form.custom-explanation-1-interpolation-statement-url')" target="_blank" rel="noopener">
{{i18n.t("overpass-form.custom-explanation-1-interpolation-statement")}}
</a>
</template>
<template #out>
<code>out</code>
</template>
</T>
</p>
<p>
<T k="overpass-form.custom-explanation-2">
<template #parking>
<code>nwr[amenity=parking]</code>
</template>
<template #atm>
<code>(nwr[amenity=atm];nwr[amenity=bank][atm][atm!=no];)</code>
</template>
</T>
</p>
</template>
<hr />
<div class="btn-toolbar">
<button
type="button"
class="btn btn-secondary"
:class="{ active: mapContext.overpassIsCustom }"
@click="toggleIsCustom()"
>{{i18n.t("overpass-form.custom-query")}}</button>
</div>
</div>
</template>
<style lang="scss">
.fm-overpass-form {
display: flex;
flex-direction: column;
min-height: 0;
.checkbox-grid {
column-width: 160px;
padding: 0 .25rem;
}
pre {
color: inherit;
font-size: inherit;
}
fieldset + hr, p + hr {
margin-top: 0;
}
.nav-link {
display: flex;
gap: 5px;
align-items: center;
}
.fm-search-box-collapse-point {
min-height: 1.5em;
}
}
</style>