feature: navigation in pokemon page
This commit is contained in:
61
package-lock.json
generated
61
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"colorthief": "^2.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^5.0.2"
|
||||
@@ -2797,6 +2798,66 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.5.0",
|
||||
"vue": "^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/@vue/devtools-api": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
|
||||
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-kit": "^7.7.9"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/@vue/devtools-kit": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
||||
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-shared": "^7.7.9",
|
||||
"birpc": "^2.3.0",
|
||||
"hookable": "^5.5.3",
|
||||
"mitt": "^3.0.1",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"speakingurl": "^14.0.1",
|
||||
"superjson": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/@vue/devtools-shared": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
||||
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rfdc": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"colorthief": "^2.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^5.0.2"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { appState } from '../store'
|
||||
import { usePokemonListStore } from '../stores/pokemonListStore'
|
||||
import { MAX_SEARCH_LENGTH } from '../constants/ui'
|
||||
|
||||
const searchQuery = ref('')
|
||||
@@ -16,6 +17,7 @@ export const homeMenuState = ref({
|
||||
export function usePokedexNavigation() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const pokemonListStore = usePokemonListStore()
|
||||
|
||||
function handleKeypad(num) {
|
||||
if (searchQuery.value.length < MAX_SEARCH_LENGTH) {
|
||||
@@ -73,10 +75,10 @@ export function usePokedexNavigation() {
|
||||
function handleMainButton() {
|
||||
if (route.path === '/' && homeMenuState.value.onSelect) {
|
||||
homeMenuState.value.onSelect()
|
||||
} else if (route.path === '/pokemon' && pokemonListStore.onSelect) {
|
||||
pokemonListStore.onSelect()
|
||||
} else if (searchQuery.value) {
|
||||
executeSearch()
|
||||
} else {
|
||||
goHome()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +91,8 @@ export function usePokedexNavigation() {
|
||||
function handleNext() {
|
||||
if (route.path === '/' && homeMenuState.value.onMoveDown) {
|
||||
homeMenuState.value.onMoveDown()
|
||||
} else if (route.path === '/pokemon' && pokemonListStore.onMoveDown) {
|
||||
pokemonListStore.onMoveDown()
|
||||
} else if (appState.currentPokemon) {
|
||||
let nextId = appState.currentPokemon.id + 1
|
||||
router.push(`/pokemon/${nextId}`)
|
||||
@@ -98,6 +102,8 @@ export function usePokedexNavigation() {
|
||||
function handlePrev() {
|
||||
if (route.path === '/' && homeMenuState.value.onMoveUp) {
|
||||
homeMenuState.value.onMoveUp()
|
||||
} else if (route.path === '/pokemon' && pokemonListStore.onMoveUp) {
|
||||
pokemonListStore.onMoveUp()
|
||||
} else if (appState.currentPokemon && appState.currentPokemon.id > 1) {
|
||||
let prevId = appState.currentPokemon.id - 1
|
||||
router.push(`/pokemon/${prevId}`)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
|
||||
router.isReady().then(() => {
|
||||
|
||||
@@ -8,9 +8,11 @@ import PageLayout from '../components/layout/PageLayout.vue'
|
||||
import ColorThief from 'colorthief'
|
||||
import { generateThemeColors } from '../utils/theme'
|
||||
import { appState } from '../store'
|
||||
import { usePokemonListStore } from '../stores/pokemonListStore'
|
||||
import { useScreenActions } from '../composables/useScreenActions'
|
||||
|
||||
const { setButtonActions } = useScreenActions()
|
||||
const pokemonListStore = usePokemonListStore()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -53,6 +55,7 @@ function getPokemonData() {
|
||||
pokemon.value = json
|
||||
pokemonImage.value = pokemon.value.sprites.other['official-artwork'].front_default
|
||||
appState.setCurrentPokemon(json)
|
||||
pokemonListStore.setLastSelectedPokemon(currentId)
|
||||
getPokemonColors(pokemonImage.value)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,24 +1,306 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { appState } from '../store'
|
||||
import PageLayout from '../components/layout/PageLayout.vue'
|
||||
import { usePokemonListStore } from '../stores/pokemonListStore'
|
||||
import { useScreenActions } from '../composables/useScreenActions'
|
||||
|
||||
const router = useRouter()
|
||||
const { setButtonActions } = useScreenActions()
|
||||
const pokemonListStore = usePokemonListStore()
|
||||
|
||||
const pokemonList = ref([])
|
||||
const selectedIndex = ref(0)
|
||||
const scrollContainer = ref(null)
|
||||
const loading = ref(true)
|
||||
const loadingMore = ref(false)
|
||||
|
||||
const TOTAL_POKEMON = 1025
|
||||
const ITEM_HEIGHT = 92
|
||||
const INITIAL_LOAD = 20
|
||||
const LOAD_AHEAD_BUFFER = 10
|
||||
const BATCH_SIZE = 10
|
||||
|
||||
async function loadPokemonBatch(startId, count) {
|
||||
const promises = []
|
||||
const endId = Math.min(startId + count, TOTAL_POKEMON + 1)
|
||||
|
||||
for (let i = startId; i < endId; i++) {
|
||||
promises.push(
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon/${i}`)
|
||||
.then(res => res.ok ? res.json() : null)
|
||||
.catch(() => null)
|
||||
)
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises)
|
||||
return results.filter(p => p !== null)
|
||||
}
|
||||
|
||||
async function loadInitialPokemon() {
|
||||
loading.value = true
|
||||
|
||||
const lastId = pokemonListStore.lastSelectedPokemonId
|
||||
|
||||
if (lastId && lastId > INITIAL_LOAD) {
|
||||
const batchesToLoad = Math.ceil(lastId / BATCH_SIZE)
|
||||
const pokemonToLoad = Math.min(batchesToLoad * BATCH_SIZE, TOTAL_POKEMON)
|
||||
const initial = await loadPokemonBatch(1, pokemonToLoad)
|
||||
pokemonList.value = initial
|
||||
} else {
|
||||
const initial = await loadPokemonBatch(1, INITIAL_LOAD)
|
||||
pokemonList.value = initial
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
|
||||
if (lastId) {
|
||||
const index = pokemonList.value.findIndex(p => p.id === lastId)
|
||||
if (index !== -1) {
|
||||
selectedIndex.value = index
|
||||
nextTick(() => scrollToSelected())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMorePokemon() {
|
||||
if (loadingMore.value || pokemonList.value.length >= TOTAL_POKEMON) return
|
||||
|
||||
loadingMore.value = true
|
||||
const nextId = pokemonList.value.length + 1
|
||||
const newPokemon = await loadPokemonBatch(nextId, BATCH_SIZE)
|
||||
pokemonList.value = [...pokemonList.value, ...newPokemon]
|
||||
loadingMore.value = false
|
||||
}
|
||||
|
||||
function checkAndLoadMore() {
|
||||
const remainingPokemon = pokemonList.value.length - selectedIndex.value - 1
|
||||
|
||||
if (remainingPokemon < LOAD_AHEAD_BUFFER && pokemonList.value.length < TOTAL_POKEMON) {
|
||||
loadMorePokemon()
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToSelected() {
|
||||
if (!scrollContainer.value) return
|
||||
|
||||
nextTick(() => {
|
||||
const container = scrollContainer.value
|
||||
const itemElement = container.children[0]?.children[selectedIndex.value]
|
||||
|
||||
if (itemElement) {
|
||||
const containerHeight = container.clientHeight
|
||||
const itemTop = itemElement.offsetTop
|
||||
const itemHeight = ITEM_HEIGHT
|
||||
const scrollTop = itemTop - (containerHeight / 2) + (itemHeight / 2)
|
||||
|
||||
container.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function moveUp() {
|
||||
if (selectedIndex.value > 0) {
|
||||
selectedIndex.value--
|
||||
scrollToSelected()
|
||||
}
|
||||
}
|
||||
|
||||
function moveDown() {
|
||||
if (selectedIndex.value < pokemonList.value.length - 1) {
|
||||
selectedIndex.value++
|
||||
scrollToSelected()
|
||||
checkAndLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
function selectPokemon() {
|
||||
const pokemon = pokemonList.value[selectedIndex.value]
|
||||
if (pokemon) {
|
||||
pokemonListStore.setLastSelectedPokemon(pokemon.id)
|
||||
router.push(`/pokemon/${pokemon.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedIndex, () => {
|
||||
checkAndLoadMore()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
appState.setPageDisplay(
|
||||
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5l7.51-3.22-7.52-3.22 3.22 7.52 3.21-7.52-6.42 6.44z',
|
||||
'POKÉMON'
|
||||
)
|
||||
|
||||
setButtonActions([
|
||||
{ buttonNumber: 1, label: 'BACK', action: () => router.push('/') }
|
||||
])
|
||||
|
||||
pokemonListStore.setNavigationHandlers({
|
||||
selectedIndex: selectedIndex.value,
|
||||
onSelect: selectPokemon,
|
||||
onMoveUp: moveUp,
|
||||
onMoveDown: moveDown
|
||||
})
|
||||
|
||||
loadInitialPokemon()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
pokemonListStore.clearNavigationHandlers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
title="POKÉMON"
|
||||
badge=""
|
||||
subtitle=""
|
||||
subtitle="Global Database"
|
||||
statusText="Browse all Pokémon species"
|
||||
>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<p class="text-zinc-500 text-sm">Coming Soon</p>
|
||||
<div v-if="loading" class="flex items-center justify-center h-full">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="relative w-20 h-20">
|
||||
<div class="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
|
||||
<div class="absolute inset-0 border-t-2 border-red-500 rounded-full animate-spin"></div>
|
||||
<div class="absolute inset-2 border-2 border-zinc-700 rounded-full"></div>
|
||||
<div class="absolute inset-6 bg-zinc-900 rounded-full flex items-center justify-center shadow-[inset_0_0_15px_rgba(0,0,0,0.7)]">
|
||||
<div class="w-3 h-3 bg-red-500 rounded-full animate-ping"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-1">
|
||||
<div class="text-sm font-black tracking-[0.25em] text-zinc-400 mb-2">LOADING DATABASE</div>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<div class="w-1 h-1 bg-zinc-600 rounded-full animate-pulse"></div>
|
||||
<div class="w-1 h-1 bg-zinc-600 rounded-full animate-pulse" style="animation-delay: 0.2s"></div>
|
||||
<div class="w-1 h-1 bg-zinc-600 rounded-full animate-pulse" style="animation-delay: 0.4s"></div>
|
||||
</div>
|
||||
<div class="text-[10px] text-zinc-600 font-mono">Initializing Pokédex...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col h-full overflow-hidden">
|
||||
<div class="px-5 py-3 border-b-2 border-zinc-800 bg-zinc-900 shrink-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="relative">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<div class="absolute inset-0 w-2 h-2 bg-green-500 rounded-full animate-ping"></div>
|
||||
</div>
|
||||
<div class="text-[10px] text-zinc-400 uppercase tracking-[0.15em] font-bold">
|
||||
{{ pokemonList.length }} <span class="text-zinc-600">/</span> {{ TOTAL_POKEMON }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-[8px] text-zinc-600 uppercase tracking-wider">Selected</div>
|
||||
<div class="text-xs text-zinc-300 font-mono font-black px-2 py-0.5 bg-zinc-950 border border-zinc-700 rounded">
|
||||
#{{ String(pokemonList[selectedIndex]?.id || 0).padStart(4, '0') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="flex-1 overflow-y-auto custom-scrollbar p-5"
|
||||
style="scroll-behavior: smooth;"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(pokemon, index) in pokemonList"
|
||||
:key="pokemon.id"
|
||||
@click="selectedIndex = index; selectPokemon()"
|
||||
:class="[
|
||||
'flex items-center gap-4 p-3 rounded-lg border-2 transition-all duration-200 cursor-pointer relative',
|
||||
index === selectedIndex
|
||||
? 'bg-zinc-900 border-zinc-600 shadow-[0_0_20px_rgba(255,255,255,0.1)]'
|
||||
: 'bg-zinc-950 border-zinc-800 hover:border-zinc-700 hover:bg-zinc-900/60'
|
||||
]"
|
||||
:style="{ height: ITEM_HEIGHT + 'px' }"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'shrink-0 rounded-lg border flex items-center justify-center overflow-hidden transition-all duration-200',
|
||||
index === selectedIndex
|
||||
? 'w-18 h-18 border-zinc-700 bg-zinc-950 shadow-inner'
|
||||
: 'w-16 h-16 border-zinc-800 bg-black'
|
||||
]"
|
||||
>
|
||||
<img
|
||||
v-if="pokemon.sprites?.other?.['official-artwork']?.front_default"
|
||||
:src="pokemon.sprites.other['official-artwork'].front_default"
|
||||
:alt="pokemon.name"
|
||||
class="w-full h-full object-contain p-1"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div v-else class="text-zinc-700 text-xs font-bold">?</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col justify-center min-w-0">
|
||||
<div class="flex items-baseline gap-2 mb-1">
|
||||
<div
|
||||
:class="[
|
||||
'font-black uppercase tracking-tight truncate transition-all duration-200',
|
||||
index === selectedIndex
|
||||
? 'text-base text-white'
|
||||
: 'text-sm text-zinc-400'
|
||||
]"
|
||||
>
|
||||
{{ pokemon.name }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'font-mono font-bold shrink-0 transition-all duration-200',
|
||||
index === selectedIndex
|
||||
? 'text-[10px] text-zinc-500'
|
||||
: 'text-[9px] text-zinc-600'
|
||||
]"
|
||||
>
|
||||
#{{ String(pokemon.id).padStart(4, '0') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
v-for="type in pokemon.types"
|
||||
:key="type.slot"
|
||||
:class="[
|
||||
'uppercase font-bold rounded border transition-all duration-200',
|
||||
index === selectedIndex
|
||||
? 'text-[9px] px-2 py-0.5 border-zinc-700 bg-zinc-800 text-zinc-300'
|
||||
: 'text-[8px] px-1.5 py-0.5 border-zinc-800 bg-zinc-950 text-zinc-500'
|
||||
]"
|
||||
>
|
||||
{{ type.type.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="index === selectedIndex"
|
||||
:class="[
|
||||
'shrink-0 w-1 h-12 rounded-full bg-white shadow-[0_0_10px_rgba(255,255,255,0.5)]'
|
||||
]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingMore" class="flex items-center justify-center py-8">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div class="relative w-10 h-10">
|
||||
<div class="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
|
||||
<div class="absolute inset-0 border-t-2 border-zinc-500 rounded-full animate-spin"></div>
|
||||
<div class="absolute inset-2 border border-zinc-700 rounded-full"></div>
|
||||
</div>
|
||||
<span class="text-[10px] uppercase tracking-[0.2em] text-zinc-500 font-bold">Loading more...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
42
src/stores/pokemonListStore.js
Normal file
42
src/stores/pokemonListStore.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const usePokemonListStore = defineStore('pokemonList', {
|
||||
state: () => ({
|
||||
lastSelectedPokemonId: parseInt(localStorage.getItem('lastSelectedPokemonId')) || null,
|
||||
selectedIndex: 0,
|
||||
onSelect: null,
|
||||
onMoveUp: null,
|
||||
onMoveDown: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setLastSelectedPokemon(id) {
|
||||
this.lastSelectedPokemonId = id
|
||||
if (id) {
|
||||
localStorage.setItem('lastSelectedPokemonId', id.toString())
|
||||
} else {
|
||||
localStorage.removeItem('lastSelectedPokemonId')
|
||||
}
|
||||
},
|
||||
|
||||
setNavigationHandlers(handlers) {
|
||||
this.selectedIndex = handlers.selectedIndex || 0
|
||||
this.onSelect = handlers.onSelect || null
|
||||
this.onMoveUp = handlers.onMoveUp || null
|
||||
this.onMoveDown = handlers.onMoveDown || null
|
||||
},
|
||||
|
||||
clearNavigationHandlers() {
|
||||
this.selectedIndex = 0
|
||||
this.onSelect = null
|
||||
this.onMoveUp = null
|
||||
this.onMoveDown = null
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
hasNavigationHandlers: (state) => {
|
||||
return state.onSelect !== null || state.onMoveUp !== null || state.onMoveDown !== null
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user