mirror of
https://github.com/directus/directus.git
synced 2026-01-28 14:28:02 -05:00
Map layout and interface improvements (#8628)
* Map layout and interface improvements: * Disable drag to rotate * Add keyboard shortcut to delete items * Hide unselect button when selection is empty * Add display template setting * Fixed fitData button behaviour * Removed unused hoveredFeatureId * Added translations * Expose clearFilters to the layout.
This commit is contained in:
@@ -59,6 +59,10 @@ function createLayoutWrapper<Options, Query>(layout: LayoutConfig): Component {
|
||||
type: Function as PropType<() => Promise<void>>,
|
||||
default: null,
|
||||
},
|
||||
clearFilters: {
|
||||
type: Function as PropType<() => void>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: WRITABLE_PROPS.map((prop) => `update:${prop}` as const),
|
||||
setup(props, { emit }) {
|
||||
|
||||
@@ -145,16 +145,15 @@ export default defineComponent({
|
||||
const controls = {
|
||||
draw: new MapboxDraw(getDrawOptions(geometryType)),
|
||||
fitData: new ButtonControl('mapboxgl-ctrl-fitdata', fitDataBounds),
|
||||
navigation: new NavigationControl(),
|
||||
navigation: new NavigationControl({
|
||||
showCompass: false,
|
||||
}),
|
||||
geolocate: new GeolocateControl(),
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setupMap();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
map.remove();
|
||||
const cleanup = setupMap();
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -168,11 +167,12 @@ export default defineComponent({
|
||||
basemap,
|
||||
};
|
||||
|
||||
function setupMap() {
|
||||
function setupMap(): () => void {
|
||||
map = new Map({
|
||||
container: container.value!,
|
||||
style: style.value,
|
||||
attributionControl: false,
|
||||
dragRotate: false,
|
||||
logoPosition: 'bottom-right',
|
||||
...props.defaultView,
|
||||
...(mapboxKey ? { accessToken: mapboxKey } : {}),
|
||||
@@ -200,6 +200,7 @@ export default defineComponent({
|
||||
map.on('draw.delete', handleDrawUpdate);
|
||||
map.on('draw.update', handleDrawUpdate);
|
||||
map.on('draw.modechange', handleDrawModeChange);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -247,6 +248,11 @@ export default defineComponent({
|
||||
loadValueFromProps();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
map.remove();
|
||||
};
|
||||
}
|
||||
|
||||
function resetValue(hard: boolean) {
|
||||
@@ -383,6 +389,12 @@ export default defineComponent({
|
||||
emit('input', serialize(currentGeometry));
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if ([8, 46].includes(event.keyCode)) {
|
||||
controls.draw.trash();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1529,8 +1529,9 @@ layouts:
|
||||
invalid_geometry: Invalid geometry
|
||||
auto_location_filter: Always filter data to view bounds
|
||||
search_this_area: Search this area
|
||||
clear_data_filter: Clear Data Filter
|
||||
clear_location_filter: Clear Location Filter
|
||||
clear_data_filter: Clear data filter
|
||||
clear_location_filter: Clear location filter
|
||||
no_results_here: No results in this area
|
||||
|
||||
panels:
|
||||
metric:
|
||||
|
||||
@@ -904,10 +904,11 @@ fields:
|
||||
png: PNG
|
||||
webP: WebP
|
||||
tiff: Tiff
|
||||
basemaps: Fond de cartes
|
||||
basemaps_raster: Raster
|
||||
basemaps_tile: Raster TileJSON
|
||||
basemaps_style: Style de Mapbox
|
||||
mapbox_key: Jeton d'accès Mapbox
|
||||
mapbox_key: Clé d'accès Mapbox
|
||||
mapbox_placeholder: pk.eyJ1Ijo.....
|
||||
transforms_note: Le nom de la méthode Sharp et ses arguments. Voir https://sharp.pixelplumbing.com/api-constructor pour plus d'informations.
|
||||
additional_transforms: Transformations supplémentaires
|
||||
@@ -1500,20 +1501,21 @@ layouts:
|
||||
end_date_field: Champ Date de fin
|
||||
map:
|
||||
map: Carte
|
||||
basemap: Carte de base
|
||||
layers: Afficher...
|
||||
basemap: Fond de carte
|
||||
layers: Couches
|
||||
edit_custom_layers: Modifier la couche
|
||||
cluster_options: Options de regroupement
|
||||
cluster: Activer le clustering
|
||||
cluster_radius: Rayon de grappe
|
||||
cluster_minpoints: Taille minimale du cluster
|
||||
cluster_maxzoom: Zoom maximum pour la grappe
|
||||
cluster: Regrouper les données
|
||||
cluster_radius: Rayon de regroupement
|
||||
cluster_minpoints: Taille minimale des clusters
|
||||
cluster_maxzoom: Zoom maximum pour le regroupement
|
||||
field: Géométrie
|
||||
invalid_geometry: Géométrie invalide
|
||||
auto_location_filter: Toujours filtrer les données pour afficher les limites
|
||||
search_this_area: Chercher dans cette zone
|
||||
clear_data_filter: Réinitialiser le filtre
|
||||
clear_location_filter: Éliminer le filtre de coupon
|
||||
auto_location_filter: Toujours filter les données selon l'emprise
|
||||
clear_data_filter: Enlever les filtres avancés
|
||||
clear_location_filter: Enlever le filtre géographique
|
||||
no_results_here: Aucun résultat dans cette zone
|
||||
panels:
|
||||
metric:
|
||||
name: Indicateurs
|
||||
|
||||
@@ -88,7 +88,9 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const attributionControl = new AttributionControl({ compact: true });
|
||||
const navigationControl = new NavigationControl();
|
||||
const navigationControl = new NavigationControl({
|
||||
showCompass: false,
|
||||
});
|
||||
const geolocateControl = new GeolocateControl();
|
||||
const fitDataControl = new ButtonControl('mapboxgl-ctrl-fitdata', () => {
|
||||
emit('fitdata');
|
||||
@@ -115,6 +117,7 @@ export default defineComponent({
|
||||
container: 'map-container',
|
||||
style: style.value,
|
||||
attributionControl: false,
|
||||
dragRotate: false,
|
||||
...props.camera,
|
||||
...(mapboxKey ? { accessToken: mapboxKey } : {}),
|
||||
});
|
||||
@@ -245,6 +248,7 @@ export default defineComponent({
|
||||
newSelection?.forEach((id) => {
|
||||
map.setFeatureState({ id, source: '__directus' }, { selected: true });
|
||||
});
|
||||
boxSelectControl.showUnselect(newSelection?.length);
|
||||
}
|
||||
|
||||
function onFeatureClick(event: MapLayerMouseEvent) {
|
||||
@@ -470,6 +474,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.mapboxgl-point-popup {
|
||||
box-shadow: 10px 10px 10px solid red;
|
||||
|
||||
&.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
|
||||
border-right-color: var(--background-normal);
|
||||
}
|
||||
@@ -493,12 +499,11 @@ export default defineComponent({
|
||||
font-family: var(--family-sans-serif);
|
||||
background-color: var(--background-normal);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
#map-container.hover .mapboxgl-canvas-container {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export default defineLayout<LayoutOptions, LayoutQuery>({
|
||||
|
||||
const customLayerDrawerOpen = ref(false);
|
||||
|
||||
const displayTemplate = syncOption(layoutOptions, 'displayTemplate', undefined);
|
||||
const cameraOptions = syncOption(layoutOptions, 'cameraOptions', undefined);
|
||||
const customLayers = syncOption(layoutOptions, 'customLayers', layers);
|
||||
const autoLocationFilter = syncOption(layoutOptions, 'autoLocationFilter', false);
|
||||
@@ -120,8 +121,7 @@ export default defineLayout<LayoutOptions, LayoutQuery>({
|
||||
});
|
||||
|
||||
const template = computed(() => {
|
||||
if (info.value?.meta?.display_template) return info.value?.meta?.display_template;
|
||||
return `{{ ${primaryKeyField.value?.field} }}`;
|
||||
return displayTemplate.value || info.value?.meta?.display_template || `{{ ${primaryKeyField.value?.field} }}`;
|
||||
});
|
||||
|
||||
const queryFields = computed(() => {
|
||||
@@ -165,17 +165,22 @@ export default defineLayout<LayoutOptions, LayoutQuery>({
|
||||
function clearLocationFilter() {
|
||||
shouldUpdateCamera.value = true;
|
||||
locationFilterOutdated.value = false;
|
||||
|
||||
locationFilter.value = undefined;
|
||||
}
|
||||
|
||||
function fitGeoJSONBounds() {
|
||||
if (!geojson.value?.features.length) {
|
||||
return;
|
||||
}
|
||||
shouldUpdateCamera.value = true;
|
||||
locationFilterOutdated.value = false;
|
||||
if (geojson.value) {
|
||||
geojsonBounds.value = geojson.value.bbox;
|
||||
geojsonBounds.value = cloneDeep(geojson.value.bbox);
|
||||
}
|
||||
}
|
||||
|
||||
function clearDataFilters() {
|
||||
locationFilter.value = undefined;
|
||||
search.value = null;
|
||||
props?.clearFilters();
|
||||
}
|
||||
|
||||
const shouldUpdateCamera = ref(false);
|
||||
@@ -322,7 +327,6 @@ export default defineLayout<LayoutOptions, LayoutQuery>({
|
||||
});
|
||||
|
||||
return {
|
||||
template,
|
||||
geojson,
|
||||
directusSource,
|
||||
directusLayers,
|
||||
@@ -338,6 +342,7 @@ export default defineLayout<LayoutOptions, LayoutQuery>({
|
||||
handleSelect,
|
||||
geometryFormat,
|
||||
geometryField,
|
||||
displayTemplate,
|
||||
isGeometryFieldNative,
|
||||
cameraOptions,
|
||||
autoLocationFilter,
|
||||
@@ -365,6 +370,7 @@ export default defineLayout<LayoutOptions, LayoutQuery>({
|
||||
clearLocationFilter,
|
||||
clearDataFilters,
|
||||
locationFilter,
|
||||
fitGeoJSONBounds,
|
||||
};
|
||||
|
||||
async function resetPresetAndRefresh() {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
@featureclick="handleClick"
|
||||
@featureselect="handleSelect"
|
||||
@moveend="cameraOptionsWritable = $event"
|
||||
@fitdata="clearLocationFilter"
|
||||
@fitdata="fitGeoJSONBounds"
|
||||
/>
|
||||
|
||||
<v-button
|
||||
@@ -47,17 +47,17 @@
|
||||
</v-info>
|
||||
<v-progress-circular v-else-if="loading || geojsonLoading" indeterminate x-large class="center" />
|
||||
<v-info
|
||||
v-else-if="itemCount === 0 && (search || filter || !locationFilterOutdated)"
|
||||
v-else-if="!loading && !itemCount && !locationFilterOutdated && (search || filter || locationFilter)"
|
||||
icon="search"
|
||||
center
|
||||
:title="t('no_results')"
|
||||
:title="t('layouts.map.no_results_here')"
|
||||
>
|
||||
<template #append>
|
||||
<v-card-actions>
|
||||
<v-button :disabled="!search && !locationFilter" @click="clearDataFilters">
|
||||
<v-button :disabled="!search && !filter" @click="clearDataFilters">
|
||||
{{ t('layouts.map.clear_data_filter') }}
|
||||
</v-button>
|
||||
<v-button :disabled="locationFilterOutdated" @click="clearLocationFilter">
|
||||
<v-button :disabled="!locationFilter" @click="clearLocationFilter">
|
||||
{{ t('layouts.map.clear_location_filter') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
@@ -213,6 +213,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
fitGeoJSONBounds: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
},
|
||||
updateLocationFilter: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="field">
|
||||
<div class="type-label">{{ t('display_template') }}</div>
|
||||
<v-field-template v-model="displayTemplateWritable" :collection="collection" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<v-checkbox
|
||||
v-model="autoLocationFilterWritable"
|
||||
@@ -48,6 +53,10 @@ import { useSync } from '@directus/shared/composables';
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
geometryFields: {
|
||||
type: Array as PropType<Item[]>,
|
||||
required: true,
|
||||
@@ -84,6 +93,10 @@ export default defineComponent({
|
||||
type: Array as PropType<any[]>,
|
||||
default: undefined,
|
||||
},
|
||||
displayTemplate: {
|
||||
type: String as string | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'update:geometryField',
|
||||
@@ -102,6 +115,7 @@ export default defineComponent({
|
||||
const clusterDataWritable = useSync(props, 'clusterData', emit);
|
||||
const customLayerDrawerOpenWritable = useSync(props, 'customLayerDrawerOpen', emit);
|
||||
const customLayersWritable = useSync(props, 'customLayers', emit);
|
||||
const displayTemplateWritable = useSync(props, 'displayTemplate', emit);
|
||||
|
||||
const basemaps = getBasemapSources();
|
||||
const { basemap } = toRefs(appStore);
|
||||
@@ -113,6 +127,7 @@ export default defineComponent({
|
||||
clusterDataWritable,
|
||||
customLayerDrawerOpenWritable,
|
||||
customLayersWritable,
|
||||
displayTemplateWritable,
|
||||
basemaps,
|
||||
basemap,
|
||||
};
|
||||
|
||||
@@ -16,4 +16,5 @@ export type LayoutOptions = {
|
||||
autoLocationFilter?: boolean;
|
||||
clusterData?: boolean;
|
||||
animateOptions?: any;
|
||||
displayTemplate?: string;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
:search="search"
|
||||
:collection="collection"
|
||||
:reset-preset="resetPreset"
|
||||
:clear-filters="clearFilters"
|
||||
>
|
||||
<collections-not-found v-if="!currentCollection || collection.startsWith('directus_')" />
|
||||
<private-view
|
||||
|
||||
@@ -113,10 +113,6 @@ export class BoxSelectControl {
|
||||
const rect = container.getBoundingClientRect();
|
||||
// @ts-ignore
|
||||
return new Point(event.clientX - rect.left - container.clientLeft, event.clientY - rect.top - container.clientTop);
|
||||
// return {
|
||||
// x: event.clientX - rect.left - container.clientLeft,
|
||||
// y: event.clientY - rect.top - container.clientTop
|
||||
// };
|
||||
}
|
||||
|
||||
onKeyDown(event: KeyboardEvent): void {
|
||||
@@ -136,6 +132,10 @@ export class BoxSelectControl {
|
||||
this.map!.fire(`select.${yes ? 'enable' : 'disable'}`);
|
||||
}
|
||||
|
||||
showUnselect(yes: boolean): void {
|
||||
this.unselectButton.show(yes);
|
||||
}
|
||||
|
||||
onKeyUp(event: KeyboardEvent): void {
|
||||
if (event.key == 'Shift') {
|
||||
this.activate(false);
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface LayoutProps<Options = any, Query = any> {
|
||||
selectMode: boolean;
|
||||
readonly: boolean;
|
||||
resetPreset?: () => Promise<void>;
|
||||
clearFilters?: () => void;
|
||||
}
|
||||
|
||||
interface LayoutContext {
|
||||
|
||||
Reference in New Issue
Block a user