Markers sidebar section

This commit is contained in:
James Lyne 2022-01-17 15:15:00 +00:00
parent 9265f8a02a
commit 91739d513a
20 changed files with 611 additions and 3 deletions

View File

@ -92,6 +92,12 @@
chatErrorUnknown: 'Unexpected error while sending chat message',
chatErrorDisabled: 'Chat is not enabled',
serversHeading: 'Servers',
markersHeading: 'Markers',
markersSearchPlaceholder: 'Search markers...',
markersSkeleton: 'No markers exist for the current world',
markersSetSkeleton: 'This marker set is empty',
markersSearchSkeleton: 'No matching markers found',
markersUnnamed: '(Unnamed marker)',
worldsSkeleton: 'No maps have been configured',
playersSkeleton: 'No players are currently online',
playersTitle: 'Click to center on player\nDouble-click to follow player',
@ -142,6 +148,7 @@
logoutSuccess: 'Logged out successfully',
closeTitle: 'Close',
showMore: 'Show more'
},
ui: {

View File

@ -23,6 +23,12 @@ export const globalMessages = [
'chatErrorDisabled',
'chatErrorUnknown',
'serversHeading',
'markersHeading',
'markersSkeleton',
'markersSetSkeleton',
'markersUnnamed',
'markersSearchPlaceholder',
'markersSearchSkeleton',
'worldsSkeleton',
'playersSkeleton',
'playersTitle',
@ -67,6 +73,7 @@ export const globalMessages = [
'logoutErrorUnknown',
'logoutSuccess',
'closeTitle',
'showMore',
] as const;
export const serverMessages = [

42
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@kyvg/vue3-notification": "2.3.0",
"@soerenmartius/vue3-clipboard": "^0.1",
"leaflet": "git+https://github.com/JLyne/leaflet.git",
"lodash.debounce": "^4.0.8",
"modern-normalize": "^1.1.0",
"vue": "^3.2.21",
"vuex": "^4.0"
@ -21,6 +22,7 @@
"@types/jest": "^27.4.0",
"@types/jest-in-case": "^1.0.5",
"@types/leaflet": "1.7.8",
"@types/lodash.debounce": "^4.0.6",
"@types/node": "^17.0.8",
"@typescript-eslint/eslint-plugin": "^5.9",
"@typescript-eslint/parser": "^5.9",
@ -1611,6 +1613,21 @@
"@types/geojson": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.14.178",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
"dev": true
},
"node_modules/@types/lodash.debounce": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz",
"integrity": "sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@ -8257,6 +8274,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -13347,6 +13369,21 @@
"@types/geojson": "*"
}
},
"@types/lodash": {
"version": "4.14.178",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
"dev": true
},
"@types/lodash.debounce": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz",
"integrity": "sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@ -18368,6 +18405,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",

View File

@ -18,6 +18,7 @@
"@kyvg/vue3-notification": "2.3.0",
"@soerenmartius/vue3-clipboard": "^0.1",
"leaflet": "git+https://github.com/JLyne/leaflet.git",
"lodash.debounce": "^4.0.8",
"modern-normalize": "^1.1.0",
"vue": "^3.2.21",
"vuex": "^4.0"
@ -27,6 +28,7 @@
"@types/jest": "^27.4.0",
"@types/jest-in-case": "^1.0.5",
"@types/leaflet": "1.7.8",
"@types/lodash.debounce": "^4.0.6",
"@types/node": "^17.0.8",
"@typescript-eslint/eslint-plugin": "^5.9",
"@typescript-eslint/parser": "^5.9",

View File

@ -143,6 +143,9 @@ export default defineComponent({
case 'M':
element = 'maps';
break;
case 'I':
element = 'markers';
break;
case 'C':
element = 'chat';
break;

View File

@ -0,0 +1 @@
<svg width="286.89pt" height="286.89pt" viewBox="0 0 286.89 286.89" xmlns="http://www.w3.org/2000/svg"><path d="m80.789 0.381-78.122 110.68s84.697 175.47 87.791 173.92c3.094-1.5426 194.15-35.093 194.15-35.093l-23.978-169.3z"/></svg>

After

Width:  |  Height:  |  Size: 233 B

View File

@ -0,0 +1 @@
<svg width="286.89pt" height="286.89pt" viewBox="0 0 286.89 286.89" xmlns="http://www.w3.org/2000/svg"><circle cx="142.68" cy="143.06" r="137.35" /></svg>

After

Width:  |  Height:  |  Size: 155 B

View File

@ -0,0 +1 @@
<svg width="286.89pt" height="286.89pt" version="1.0" viewBox="0 0 286.89 286.89" xmlns="http://www.w3.org/2000/svg"><path d="m138.25 1.7383a8.5 8.5 0 0 0-6.125 2.1953l-115.74 105.05a8.5008 8.5008 0 0 0 2.084 13.98l222.54 105.04-84.184 41.338a8.5 8.5 0 0 0-3.8848 11.377 8.5 8.5 0 0 0 11.377 3.8828l100.03-49.119a8.5008 8.5008 0 0 0-0.11718-15.316l-227-107.15 106.32-96.5a8.5 8.5 0 0 0 0.58204-12.006 8.5 8.5 0 0 0-5.8828-2.7773z"/></svg>

After

Width:  |  Height:  |  Size: 439 B

View File

@ -0,0 +1,3 @@
<svg width="286.89163pt" height="286.89163pt" version="1.0" viewBox="0 0 286.89162 286.89163" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-59.399373,-0.17905527)" d="m 202.21483,0.17905527 c -49.2926,0 -89.25212,39.95956873 -89.25212,89.25210973 0,49.292555 82.37686,197.611115 89.25212,197.611115 7.99783,0 89.25211,-148.31856 89.25211,-197.611115 0,-49.292541 -39.95957,-89.25210973 -89.25211,-89.25210973 z m 0,51.39320173 c 20.91016,-0.0011 37.86223,16.948751 37.86389,37.858908 0.001,20.912125 -16.95177,37.865025 -37.86389,37.863935 -20.91214,0.001 -37.86506,-16.9518 -37.86394,-37.863935 0.002,-20.910176 16.95376,-37.86003 37.86394,-37.858908 z" />
</svg>

After

Width:  |  Height:  |  Size: 686 B

View File

@ -24,6 +24,13 @@
@click="handleSectionClick" @keydown="handleSectionKeydown">
<SvgIcon :name="mapCount > 1 ? 'maps' : 'servers'"></SvgIcon>
</button>
<button class="button--markers" data-section="markers"
:title="messageMarkers"
:aria-label="messageMarkers"
:aria-expanded="markersVisible"
@click="handleSectionClick" @keydown="handleSectionKeydown">
<SvgIcon name="marker_point"></SvgIcon>
</button>
<button v-if="playerMakersEnabled" class="button--players" data-section="players"
:title="messagePlayers" :aria-label="messagePlayers" :aria-expanded="playersVisible"
@click="handleSectionClick" @keydown="handleSectionKeydown">
@ -33,6 +40,7 @@
<div class="sidebar__content" @keydown="handleSidebarKeydown">
<ServersSection v-if="serverCount > 1" :hidden="!mapsVisible"></ServersSection>
<WorldsSection v-if="mapCount > 1" :hidden="!mapsVisible"></WorldsSection>
<MarkersSection v-if="previouslyVisible.has('markers')" :hidden="!markersVisible"></MarkersSection>
<PlayersSection v-if="playerMakersEnabled && previouslyVisible.has('players')" :hidden="!playersVisible"></PlayersSection>
<FollowTargetSection v-if="following" :hidden="!followVisible" :target="following"></FollowTargetSection>
</div>
@ -45,12 +53,14 @@ import FollowTargetSection from './sidebar/FollowTargetSection.vue';
import PlayersSection from "@/components/sidebar/PlayersSection.vue";
import ServersSection from "@/components/sidebar/ServersSection.vue";
import WorldsSection from "@/components/sidebar/WorldsSection.vue";
import MarkersSection from "@/components/sidebar/MarkersSection.vue";
import {useStore} from "@/store";
import SvgIcon from "@/components/SvgIcon.vue";
import {MutationTypes} from "@/store/mutation-types";
import "@/assets/icons/players.svg";
import "@/assets/icons/maps.svg";
import "@/assets/icons/servers.svg";
import "@/assets/icons/marker_point.svg";
import {nextTick, ref, watch} from "vue";
import {handleKeyboardEvent} from "@/util/events";
import {focus} from "@/util";
@ -58,6 +68,7 @@ import {LiveAtlasSidebarSection} from "@/index";
export default defineComponent({
components: {
MarkersSection,
WorldsSection,
ServersSection,
PlayersSection,
@ -79,12 +90,14 @@ export default defineComponent({
messageWorlds = computed(() => store.state.messages.worldsHeading),
messageServers = computed(() => store.state.messages.serversHeading),
messageMarkers = computed(() => store.state.messages.markersHeading),
messagePlayers = computed(() => store.getters.playersHeading),
playerMakersEnabled = computed(() => !!store.state.components.playerMarkers),
playersVisible = computed(() => currentlyVisible.value.has('players')),
mapsVisible = computed(() => currentlyVisible.value.has('maps')),
markersVisible = computed(() => currentlyVisible.value.has('markers')),
followVisible = computed(() => {
//Show following alongside playerlist on small screens
return (!smallScreen.value && following.value)
@ -132,6 +145,7 @@ export default defineComponent({
//Focus sidebar sections when they become visible, except on initial load
watch(playersVisible, newValue => newValue && !firstLoad.value && nextTick(() => focusSection('players')));
watch(mapsVisible, newValue => newValue && !firstLoad.value && nextTick(() => focusSection('maps')));
watch(markersVisible, newValue => newValue && !firstLoad.value && nextTick(() => focusSection('markers')));
return {
sidebar,
@ -142,11 +156,13 @@ export default defineComponent({
messageWorlds,
messageServers,
messageMarkers,
messagePlayers,
previouslyVisible,
playersVisible,
mapsVisible,
markersVisible,
followVisible,
playerMakersEnabled,

View File

@ -0,0 +1,168 @@
<!--
- Copyright 2022 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<input ref="searchInput" v-if="search && unfilteredTotal" id="markers__search" class="section__search" type="text"
name="search" :value="searchQuery" :placeholder="messageMarkersSearchPlaceholder"
@keydown="e => e.stopImmediatePropagation()" @input="onSearchInput">
<RadioList v-if="markers.size" v-bind="$attrs" @keydown="onListKeydown">
<MarkerListItem v-for="[id, marker] in markers" :key="id" :marker="marker" :id="id"></MarkerListItem>
<button type="button" ref="showMoreButton" v-if="viewLimit < total" @click.prevent="showMore">{{ messageShowMore }}</button>
</RadioList>
<div v-else-if="searchQuery" class="section__skeleton" v-bind="$attrs">{{ messageSkeletonMarkersSearch }}</div>
<div v-else class="section__skeleton" v-bind="$attrs">{{ messageSkeletonMarkers }}</div>
</template>
<script lang="ts">
import {defineComponent, onMounted, reactive, ref} from 'vue';
import debounce from 'lodash.debounce';
import RadioList from "@/components/util/RadioList.vue";
import {LiveAtlasMarkerSet, LiveAtlasMarker} from "@/index";
import {nonReactiveState} from "@/store/state";
import {computed, onUnmounted, watch} from "@vue/runtime-core";
import {useStore} from "@/store";
import MarkerListItem from "@/components/list/MarkerListItem.vue";
import {registerSetUpdateHandler, unregisterSetUpdateHandler} from "@/util/markers";
import {DynmapMarkerUpdate} from "@/dynmap";
export default defineComponent({
name: 'MarkerList',
components: {
MarkerListItem,
RadioList,
},
props: {
markerSet: {
type: Object as () => LiveAtlasMarkerSet,
required: true
},
search: {
type: Boolean,
default: true,
}
},
setup(props) {
let focusFrame = 0;
const store = useStore(),
messageShowMore = computed(() => store.state.messages.showMore),
messageMarkersSearchPlaceholder = computed(() => store.state.messages.markersSearchPlaceholder),
messageSkeletonMarkersSearch = computed(() => store.state.messages.markersSearchSkeleton),
messageSkeletonMarkers = computed(() => store.state.messages.markersSetSkeleton),
setContents = nonReactiveState.markers.get(props.markerSet.id)!,
searchQuery = ref(""),
markers = ref<Map<string, LiveAtlasMarker>>(new Map()),
showMoreButton = ref<HTMLButtonElement | null>(null),
total = ref(0),
unfilteredTotal = ref(0),
viewLimit = ref(50),
searchInput = ref<HTMLInputElement | null>(null);
const onListKeydown = (e: KeyboardEvent) => {
if(e.key === 'f' && e.ctrlKey) {
e.preventDefault();
searchInput.value!.focus();
}
}
const getMarkers = () => {
markers.value.clear();
if(!setContents) {
return;
}
unfilteredTotal.value = setContents.size;
let count = 0;
setContents.forEach((marker, id) => {
if(searchQuery.value && !marker.tooltip.toLowerCase().includes(searchQuery.value)) {
return;
}
count++;
if(count < viewLimit.value) {
markers.value.set(id, reactive(marker));
}
});
total.value = setContents.size;
};
const showMore = () => {
const lastLabel = (showMoreButton.value as HTMLButtonElement).previousElementSibling as HTMLLabelElement;
viewLimit.value += 50;
//Focus first new list item
focusFrame = requestAnimationFrame(() => (lastLabel.nextElementSibling as HTMLInputElement).focus());
};
const handleUpdate = (update: DynmapMarkerUpdate) => {
unfilteredTotal.value = setContents.size;
if(update.removed ||
(searchQuery.value && !update.payload.tooltip.toLowerCase().includes(searchQuery.value))) {
markers.value.delete(update.id);
} else if(markers.value.has(update.id) || markers.value.size < viewLimit.value) {
markers.value.set(update.id, update.payload);
}
};
const onSearchInput = (e: Event) => {
searchQuery.value = (e.target as HTMLInputElement).value.toLowerCase();
};
const debouncedSearch = debounce(() => {
viewLimit.value = 50;
getMarkers();
searchInput.value!.nextElementSibling!.scrollIntoView();
}, 100);
getMarkers();
watch(viewLimit, () => getMarkers());
watch(searchQuery, () => debouncedSearch());
onMounted(() => registerSetUpdateHandler(handleUpdate, props.markerSet.id));
onUnmounted(() => {
if(focusFrame) {
cancelAnimationFrame(focusFrame);
}
debouncedSearch.cancel();
unregisterSetUpdateHandler(handleUpdate, props.markerSet.id);
});
return {
messageShowMore,
messageMarkersSearchPlaceholder,
messageSkeletonMarkersSearch,
messageSkeletonMarkers,
searchQuery,
markers,
showMoreButton,
viewLimit,
total,
unfilteredTotal,
searchInput,
showMore,
onListKeydown,
onSearchInput
}
}
});
</script>

View File

@ -0,0 +1,120 @@
<!--
- Copyright 2022 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<input :id="`marker-${id}`" type="radio" name="marker" v-bind:value="id" @click.prevent="pan">
<label :for="`marker-${id}`" class="marker" :title="marker.tooltip" @click.prevent="pan">
<img width="16" height="16" v-if="icon" class="marker__icon" :src="icon" alt="" />
<SvgIcon v-else :name="defaultIcon" class="marker__icon"></SvgIcon>
<span class="marker__label">{{ marker.tooltip || messageUnnamed }}</span>
<span class="marker__location">X: {{ marker.location.x }}, Z: {{ marker.location.z }}</span>
</label>
</template>
<script lang="ts">
import {defineComponent} from 'vue';
import {LiveAtlasMarker, LiveAtlasPathMarker, LiveAtlasPointMarker} from "@/index";
import {computed} from "@vue/runtime-core";
import SvgIcon from "@/components/SvgIcon.vue";
import "@/assets/icons/marker_point.svg";
import "@/assets/icons/marker_line.svg";
import "@/assets/icons/marker_area.svg";
import "@/assets/icons/marker_circle.svg";
import {useStore} from "vuex";
import {LiveAtlasMarkerType} from "@/util/markers";
import {MutationTypes} from "@/store/mutation-types";
export default defineComponent({
name: 'MarkerListItem',
components: {SvgIcon},
props: {
id: {
type: String,
required: true,
},
marker: {
type: Object as () => LiveAtlasMarker,
required: true
}
},
setup(props) {
const store = useStore(),
messageUnnamed = computed(() => store.state.messages.markersUnnamed),
icon = computed(() => {
if('icon' in props.marker) {
return store.state.currentMapProvider!.getMarkerIconUrl((props.marker as LiveAtlasPointMarker).icon);
}
return undefined;
}),
defaultIcon = computed(() => {
switch(props.marker.type) {
case LiveAtlasMarkerType.POINT:
return 'marker_point';
case LiveAtlasMarkerType.AREA:
return 'marker_area';
case LiveAtlasMarkerType.LINE:
return 'marker_line';
case LiveAtlasMarkerType.CIRCLE:
return 'marker_circle';
}
});
const pan = () => {
if(props.marker.type === LiveAtlasMarkerType.POINT) {
store.commit(MutationTypes.SET_VIEW_TARGET, {
location: props.marker.location,
});
} else {
store.commit(MutationTypes.SET_VIEW_TARGET, {
location: (props.marker as LiveAtlasPathMarker).bounds,
options: {
padding: [10, 10]
}
});
}
}
return {
icon,
defaultIcon,
messageUnnamed,
pan,
}
}
});
</script>
<style lang="scss" scoped>
input[type=radio] + .marker {
padding-left: 3.9rem;
.marker__icon {
max-width: 1.6rem;
position: absolute;
top: 0;
left: 0.8rem;
bottom: 0;
margin: auto;
}
.marker__location {
font-size: 1.4rem;
font-family: monospace;
}
}
</style>

View File

@ -0,0 +1,162 @@
<!--
- Copyright 2022 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<RadioList ref="list" v-if="!currentSet" :aria-labelledby="ariaLabelledby">
<template v-for="[id, markerSet] in markerSets" :key="id">
<input :id="`marker-set-${id}`" type="radio" name="marker-set" v-model="currentSet" v-bind:value="markerSet">
<label :for="`marker-set-${id}`">
<span>{{ markerSet.label || id }}</span>
<span>{{ markerCounts.get(markerSet) }} Marker(s)</span>
</label>
</template>
</RadioList>
<template v-else>
<div ref="subHeader" class="markers__header">
<button ref="backButton" class="markers__back" @click.prevent="currentSet = undefined">
<SvgIcon name="arrow"></SvgIcon>
</button>
<h3 class="markers__set">{{ currentSet.label }}</h3>
</div>
<MarkerList ref="submenu" :marker-set="currentSet" @keydown="onSubmenuKeydown"></MarkerList>
</template>
</template>
<script lang="ts">
import {ComponentPublicInstance, defineComponent, nextTick, onMounted, ref} from 'vue';
import RadioList from "@/components/util/RadioList.vue";
import {LiveAtlasMarkerSet} from "@/index";
import {nonReactiveState} from "@/store/state";
import {onUnmounted, watch} from "@vue/runtime-core";
import MarkerList from "@/components/list/MarkerList.vue";
import SvgIcon from "@/components/SvgIcon.vue";
import {registerUpdateHandler, unregisterUpdateHandler} from "@/util/markers";
import {DynmapMarkerUpdate} from "@/dynmap";
export default defineComponent({
name: 'MarkerSetList',
components: {
SvgIcon,
MarkerList,
RadioList,
},
props: {
markerSets: {
type: Object as () => Map<string, LiveAtlasMarkerSet>,
required: true
},
ariaLabelledby: {
type: String,
default: '',
}
},
setup(props) {
const markerCounts = ref<Map<LiveAtlasMarkerSet, number>>(new Map()),
currentSet = ref<LiveAtlasMarkerSet | undefined>(undefined),
list = ref<ComponentPublicInstance | null>(null),
subHeader = ref<HTMLElement | null>(null),
backButton = ref<HTMLButtonElement | null>(null);
const checkSets = () => {
props.markerSets?.forEach((set) => checkSet(set));
};
const checkSet = (set: LiveAtlasMarkerSet) => {
const markers = nonReactiveState.markers.get(set.id),
markerCount = markers ? markers.size : 0;
markerCounts.value.set(set, markerCount);
};
const handleUpdate = (update: DynmapMarkerUpdate) => {
checkSet(props.markerSets.get(update.set)!);
}
const onSubmenuKeydown = (e: KeyboardEvent) => {
if(e.key === 'Backspace') {
currentSet.value = undefined;
e.preventDefault();
}
}
const updateFocus = (newValue?: LiveAtlasMarkerSet, oldValue?: LiveAtlasMarkerSet) => {
let focusTarget;
if(newValue) {
focusTarget = subHeader.value!.parentNode!.querySelector('.menu input') || backButton.value;
} else if(oldValue) {
focusTarget = list.value!.$el.parentNode.querySelector(`[id="marker-set-${oldValue.id}"]`);
}
if(focusTarget) {
(focusTarget as HTMLElement).focus();
}
}
watch(props.markerSets, () => checkSets);
watch(currentSet, (newValue, oldValue) => nextTick(() => updateFocus(newValue, oldValue)));
onMounted(() => {
checkSets();
registerUpdateHandler(handleUpdate);
});
onUnmounted(() => {
unregisterUpdateHandler(handleUpdate);
});
return {
markerCounts,
currentSet,
list,
subHeader,
backButton,
onSubmenuKeydown,
}
}
});
</script>
<style lang="scss" scoped>
.markers__back {
width: 3.2rem;
height: 3.2rem;
flex-grow: 0;
margin-right: 1rem;
transform: rotate(90deg);
}
.markers__header {
display: flex;
align-items: center;
padding-bottom: 1rem;
position: sticky;
top: 4.8rem;
background-color: var(--background-base);
z-index: 3;
}
.markers__set {
flex: 1 1 auto;
min-width: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin: 0;
}
</style>

View File

@ -0,0 +1,61 @@
<!--
- Copyright 2022 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<SidebarSection v-if="markerSets.size" name="markers" class="markers">
<template v-slot:heading>{{ heading }}</template>
<template v-slot:default>
<MarkerSetList :markerSets="markerSets" aria-labelledby="markers-heading"></MarkerSetList>
</template>
</SidebarSection>
</template>
<script lang="ts">
import {computed, defineComponent} from 'vue';
import {useStore} from "@/store";
import SidebarSection from "@/components/sidebar/SidebarSection.vue";
import MarkerSetList from "@/components/list/MarkerSetList.vue";
export default defineComponent({
name: 'MarkersSection',
components: {
MarkerSetList,
SidebarSection,
},
setup() {
const store = useStore(),
heading = computed(() => store.state.messages.markersHeading),
markerSets = computed(() => store.state.markerSets);
return {
heading,
markerSets
}
}
});
</script>
<style lang="scss" scoped>
::v-deep(.menu), ::v-deep(.menu input) {
scroll-margin-top: 14.4rem;
scroll-margin-bottom: 6.5rem;
}
::v-deep(.section__search) {
top: 9rem;
}
</style>

2
src/index.d.ts vendored
View File

@ -126,7 +126,7 @@ interface LiveAtlasUIConfig {
export type LiveAtlasUIElement = 'layers' | 'chat' | LiveAtlasSidebarSection;
export type LiveAtlasUIModal = 'login' | 'settings';
export type LiveAtlasSidebarSection = 'servers' | 'players' | 'maps';
export type LiveAtlasSidebarSection = 'servers' | 'players' | 'maps' | 'markers';
export type LiveAtlasDimension = 'overworld' | 'nether' | 'end';
export type LiveAtlasSidebarSectionState = {

View File

@ -61,6 +61,13 @@
margin: auto;
}
}
> span {
display: block;
text-overflow: inherit;
overflow: inherit;
white-space: inherit;
}
}
@mixin button-hovered {

View File

@ -18,7 +18,7 @@
* players on the map
*/
.marker {
.map .marker {
display: flex;
align-items: center;

View File

@ -262,13 +262,18 @@ input {
}
}
& > li > button, & > input[type=radio] + label {
& > li > button, & > input[type=radio] + label, & > button {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
text-align: left;
border-radius: 0.5rem;
}
& > button {
width: 100%;
text-align: center;
}
}
//noinspection CssOverwrittenProperties

View File

@ -90,6 +90,7 @@ export const actions: ActionTree<State, State> & Actions = {
//Make UI visible if configured, there's enough space to do so, and this is the first config load
if(!state.ui.visibleElements.size && state.configuration.expandUI && !state.ui.smallScreen) {
commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'players', state: true});
commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'markers', state: true});
commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'maps', state: true});
}

View File

@ -215,6 +215,7 @@ export const state: State = {
servers: {},
players: {},
maps: {},
markers: {},
},
}
};