Allow sidebar sections to be collapsed

This commit is contained in:
James Lyne 2021-05-25 19:26:14 +01:00
parent 22102eae65
commit eaecba10c6
11 changed files with 139 additions and 46 deletions

View File

@ -108,6 +108,7 @@
locationChunk: 'Chunk', locationChunk: 'Chunk',
contextMenuCopyLink: 'Copy link to here', contextMenuCopyLink: 'Copy link to here',
contextMenuCenterHere: 'Center here', contextMenuCenterHere: 'Center here',
toggleTitle: 'Click to toggle this section',
} }
}; };
</script> </script>

View File

@ -95,6 +95,7 @@ function buildMessagesConfig(response: any): LiveAtlasMessageConfig {
locationChunk: liveAtlasMessages.locationChunk || '', locationChunk: liveAtlasMessages.locationChunk || '',
contextMenuCopyLink: liveAtlasMessages.contextMenuCopyLink || '', contextMenuCopyLink: liveAtlasMessages.contextMenuCopyLink || '',
contextMenuCenterHere: liveAtlasMessages.contextMenuCenterHere || '', contextMenuCenterHere: liveAtlasMessages.contextMenuCenterHere || '',
toggleTitle: liveAtlasMessages.toggleTitle || '',
} }
} }

View File

@ -42,9 +42,9 @@ import FollowTarget from './sidebar/FollowTarget.vue';
import {useStore} from "@/store"; import {useStore} from "@/store";
import SvgIcon from "@/components/SvgIcon.vue"; import SvgIcon from "@/components/SvgIcon.vue";
import {MutationTypes} from "@/store/mutation-types"; import {MutationTypes} from "@/store/mutation-types";
import {DynmapUIElement} from "@/dynmap";
import "@/assets/icons/players.svg"; import "@/assets/icons/players.svg";
import "@/assets/icons/maps.svg"; import "@/assets/icons/maps.svg";
import {LiveAtlasUIElement} from "@/index";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -67,7 +67,7 @@ export default defineComponent({
messageWorlds = computed(() => store.state.messages.worldsHeading), messageWorlds = computed(() => store.state.messages.worldsHeading),
messagePlayers = computed(() => store.state.messages.playersHeading), messagePlayers = computed(() => store.state.messages.playersHeading),
toggleElement = (element: DynmapUIElement) => { toggleElement = (element: LiveAtlasUIElement) => {
store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, element); store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, element);
}, },
@ -168,7 +168,18 @@ export default defineComponent({
.section__heading { .section__heading {
font-size: 2rem; font-size: 2rem;
margin-bottom: 1rem; cursor: pointer;
user-select: none;
padding: 1.5rem 1.5rem 1rem;
margin: -1.5rem -1.5rem 0;
background-color: transparent;
color: inherit;
text-align: left;
&:hover, &:focus-visible, &.focus-visible, &:active {
background-color: transparent;
color: inherit;
}
} }
.section__content { .section__content {
@ -191,6 +202,17 @@ export default defineComponent({
align-self: center; align-self: center;
margin-top: 1rem; margin-top: 1rem;
} }
&.section--collapsed {
.section__heading {
padding-bottom: 1.5rem;
margin-bottom: -1.5rem;
}
.section__content {
display: none;
}
}
} }
@media (max-width: 480px) { @media (max-width: 480px) {

View File

@ -0,0 +1,58 @@
<template>
<section :class="{'sidebar__section': true, 'section--collapsed': collapsed}">
<button type="button" class="section__heading"
@click.prevent="toggle" @keypress.prevent="handleKeypress" :title="title">
<slot name="heading"></slot>
</button>
<ul class="section__content menu">
<slot></slot>
</ul>
</section>
</template>
<script lang="ts">
import {useStore} from "@/store";
import {LiveAtlasSidebarSection} from "@/index";
import {defineComponent} from "@vue/runtime-core";
export default defineComponent({
name: 'CollapsibleSection',
props: {
name: {
type: Object as () => LiveAtlasSidebarSection,
required: true,
}
},
computed: {
title(): string {
return useStore().state.messages.toggleTitle;
},
collapsed(): boolean {
return useStore().state.ui.collapsedSections.has(this.name);
},
},
methods: {
handleKeypress(e: KeyboardEvent) {
if(e.key !== ' ' && e.key !== 'Enter') {
return;
}
this.toggle();
},
toggle() {
const store = useStore();
if(this.collapsed) {
store.state.ui.collapsedSections.delete(this.name);
} else {
store.state.ui.collapsedSections.add(this.name);
}
}
}
});
</script>

View File

@ -15,22 +15,26 @@
--> -->
<template> <template>
<section class="sidebar__section sidebar__section--players"> <CollapsibleSection name="players">
<span class="section__heading">{{ heading }} [{{ players.size }}/{{ maxPlayers }}]</span> <template v-slot:heading>{{ heading }} [{{ players.size }}/{{ maxPlayers }}]</template>
<ul class="section__content menu"> <template v-slot:default>
<PlayerListItem v-for="[account, player] in players" :key="account" :player="player"></PlayerListItem> <ul class="section__content menu">
<li v-if="!players.size" class="section__skeleton">{{ skeletonPlayers }}</li> <PlayerListItem v-for="[account, player] in players" :key="account" :player="player"></PlayerListItem>
</ul> <li v-if="!players.size" class="section__skeleton">{{ skeletonPlayers }}</li>
</section> </ul>
</template>
</CollapsibleSection>
</template> </template>
<script lang="ts"> <script lang="ts">
import PlayerListItem from "./PlayerListItem.vue"; import PlayerListItem from "./PlayerListItem.vue";
import {defineComponent} from "@vue/runtime-core"; import {defineComponent} from "@vue/runtime-core";
import {useStore} from "@/store"; import {useStore} from "@/store";
import CollapsibleSection from "@/components/sidebar/CollapsibleSection.vue";
export default defineComponent({ export default defineComponent({
components: { components: {
CollapsibleSection,
PlayerListItem PlayerListItem
}, },
@ -50,7 +54,7 @@ export default defineComponent({
maxPlayers(): number { maxPlayers(): number {
return useStore().state.configuration.maxPlayers; return useStore().state.configuration.maxPlayers;
} }
}, }
}); });
</script> </script>

View File

@ -15,22 +15,26 @@
--> -->
<template> <template>
<section class="sidebar__section" v-if="servers.size > 1"> <CollapsibleSection v-if="servers.size > 1" name="servers">
<span class="section__heading">{{ heading }}</span> <template v-slot:heading>{{ heading }}</template>
<ul class="section__content menu"> <template v-slot:default>
<ServerListItem :server="server" v-for="[name, server] in servers" :key="name"></ServerListItem> <ul class="section__content menu">
</ul> <ServerListItem :server="server" v-for="[name, server] in servers" :key="name"></ServerListItem>
</section> </ul>
</template>
</CollapsibleSection>
</template> </template>
<script lang="ts"> <script lang="ts">
import ServerListItem from './ServerListItem.vue'; import ServerListItem from './ServerListItem.vue';
import {defineComponent} from 'vue'; import {defineComponent} from 'vue';
import {useStore} from "@/store"; import {useStore} from "@/store";
import CollapsibleSection from "@/components/sidebar/CollapsibleSection.vue";
export default defineComponent({ export default defineComponent({
name: 'ServerList', name: 'ServerList',
components: { components: {
CollapsibleSection,
ServerListItem ServerListItem
}, },
@ -42,7 +46,6 @@ export default defineComponent({
servers() { servers() {
return useStore().state.servers; return useStore().state.servers;
} }
}, }
}); });
</script> </script>

View File

@ -15,23 +15,27 @@
--> -->
<template> <template>
<section class="sidebar__section"> <CollapsibleSection name="maps">
<span class="section__heading">{{ heading }}</span> <template v-slot:heading>{{ heading }}</template>
<ul class="section__content"> <template v-slot:default>
<WorldListItem :world="world" v-for="[name, world] in worlds" :key="name"></WorldListItem> <ul class="section__content">
<li v-if="!worlds.size" class="section__skeleton">{{ skeletonWorlds }}</li> <WorldListItem :world="world" v-for="[name, world] in worlds" :key="name"></WorldListItem>
</ul> <li v-if="!worlds.size" class="section__skeleton">{{ skeletonWorlds }}</li>
</section> </ul>
</template>
</CollapsibleSection>
</template> </template>
<script lang="ts"> <script lang="ts">
import WorldListItem from './WorldListItem.vue'; import WorldListItem from './WorldListItem.vue';
import {defineComponent} from 'vue'; import {defineComponent} from 'vue';
import {useStore} from "@/store"; import {useStore} from "@/store";
import CollapsibleSection from "@/components/sidebar/CollapsibleSection.vue";
export default defineComponent({ export default defineComponent({
name: 'WorldList', name: 'WorldList',
components: { components: {
CollapsibleSection,
WorldListItem WorldListItem
}, },
@ -47,11 +51,6 @@ export default defineComponent({
worlds() { worlds() {
return useStore().state.worlds; return useStore().state.worlds;
} }
}, }
}); });
</script> </script>
<style scoped>
</style>

2
src/dynmap.d.ts vendored
View File

@ -296,5 +296,3 @@ interface DynmapChat {
source?: string; source?: string;
timestamp: number; timestamp: number;
} }
export type DynmapUIElement = 'chat' | 'players' | 'maps' | 'settings';

4
src/index.d.ts vendored
View File

@ -63,4 +63,8 @@ interface LiveAtlasMessageConfig {
locationChunk: string; locationChunk: string;
contextMenuCopyLink: string; contextMenuCopyLink: string;
contextMenuCenterHere: string; contextMenuCenterHere: string;
toggleTitle: string;
} }
export type LiveAtlasUIElement = 'chat' | 'players' | 'maps' | 'settings';
export type LiveAtlasSidebarSection = 'servers' | 'players' | 'maps';

View File

@ -28,10 +28,10 @@ import {
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapTileUpdate, DynmapServerConfig, DynmapTileUpdate,
DynmapWorld, DynmapWorld,
DynmapWorldState, DynmapParsedUrl, DynmapChat, DynmapUIElement DynmapWorldState, DynmapParsedUrl, DynmapChat
} from "@/dynmap"; } from "@/dynmap";
import {DynmapProjection} from "@/leaflet/projection/DynmapProjection"; import {DynmapProjection} from "@/leaflet/projection/DynmapProjection";
import {LiveAtlasMessageConfig, LiveAtlasServerDefinition} from "@/index"; import {LiveAtlasMessageConfig, LiveAtlasServerDefinition, LiveAtlasUIElement} from "@/index";
export type CurrentMapPayload = { export type CurrentMapPayload = {
worldName: string; worldName: string;
@ -80,8 +80,8 @@ export type Mutations<S = State> = {
[MutationTypes.CLEAR_PAN_TARGET](state: S, a?: void): void [MutationTypes.CLEAR_PAN_TARGET](state: S, a?: void): void
[MutationTypes.SET_SMALL_SCREEN](state: S, payload: boolean): void [MutationTypes.SET_SMALL_SCREEN](state: S, payload: boolean): void
[MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY](state: S, payload: DynmapUIElement): void [MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY](state: S, payload: LiveAtlasUIElement): void
[MutationTypes.SET_UI_ELEMENT_VISIBILITY](state: S, payload: {element: DynmapUIElement, state: boolean}): void [MutationTypes.SET_UI_ELEMENT_VISIBILITY](state: S, payload: {element: LiveAtlasUIElement, state: boolean}): void
[MutationTypes.SET_LOGGED_IN](state: S, payload: boolean): void [MutationTypes.SET_LOGGED_IN](state: S, payload: boolean): void
} }
@ -509,7 +509,7 @@ export const mutations: MutationTree<State> & Mutations = {
state.ui.smallScreen = smallScreen; state.ui.smallScreen = smallScreen;
}, },
[MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY](state: State, element: DynmapUIElement): void { [MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY](state: State, element: LiveAtlasUIElement): void {
const newState = !state.ui.visibleElements.has(element); const newState = !state.ui.visibleElements.has(element);
if(newState && state.ui.smallScreen) { if(newState && state.ui.smallScreen) {
@ -520,7 +520,7 @@ export const mutations: MutationTree<State> & Mutations = {
newState ? state.ui.visibleElements.add(element) : state.ui.visibleElements.delete(element); newState ? state.ui.visibleElements.add(element) : state.ui.visibleElements.delete(element);
}, },
[MutationTypes.SET_UI_ELEMENT_VISIBILITY](state: State, payload: {element: DynmapUIElement, state: boolean}): void { [MutationTypes.SET_UI_ELEMENT_VISIBILITY](state: State, payload: {element: LiveAtlasUIElement, state: boolean}): void {
if(payload.state && state.ui.smallScreen) { if(payload.state && state.ui.smallScreen) {
state.ui.visibleElements.clear(); state.ui.visibleElements.clear();
} }

View File

@ -19,10 +19,10 @@ import {
DynmapWorldMap, DynmapMarkerSet, DynmapMarkerSetUpdates, DynmapWorldMap, DynmapMarkerSet, DynmapMarkerSetUpdates,
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapTileUpdate, DynmapServerConfig, DynmapTileUpdate,
DynmapWorld, DynmapWorldState, Coordinate, DynmapParsedUrl, DynmapChat, DynmapUIElement DynmapWorld, DynmapWorldState, Coordinate, DynmapParsedUrl, DynmapChat
} from "@/dynmap"; } from "@/dynmap";
import {DynmapProjection} from "@/leaflet/projection/DynmapProjection"; import {DynmapProjection} from "@/leaflet/projection/DynmapProjection";
import {LiveAtlasMessageConfig, LiveAtlasServerDefinition} from "@/index"; import {LiveAtlasMessageConfig, LiveAtlasServerDefinition, LiveAtlasSidebarSection, LiveAtlasUIElement} from "@/index";
export type State = { export type State = {
version: string; version: string;
@ -63,8 +63,9 @@ export type State = {
ui: { ui: {
smallScreen: boolean; smallScreen: boolean;
visibleElements: Set<DynmapUIElement>; visibleElements: Set<LiveAtlasUIElement>;
previouslyVisibleElements: Set<DynmapUIElement>; previouslyVisibleElements: Set<LiveAtlasUIElement>;
collapsedSections: Set<LiveAtlasSidebarSection>;
}; };
parsedUrl: DynmapParsedUrl; parsedUrl: DynmapParsedUrl;
@ -125,7 +126,8 @@ export const state: State = {
locationRegion: '', locationRegion: '',
locationChunk: '', locationChunk: '',
contextMenuCopyLink: '', contextMenuCopyLink: '',
contextMenuCenterHere: '' contextMenuCenterHere: '',
toggleTitle: '',
}, },
loggedIn: false, loggedIn: false,
@ -207,6 +209,7 @@ export const state: State = {
smallScreen: false, smallScreen: false,
visibleElements:new Set(), visibleElements:new Set(),
previouslyVisibleElements: new Set(), previouslyVisibleElements: new Set(),
collapsedSections: new Set(),
}, },
parsedUrl: { parsedUrl: {