Graph calculator improvmentes
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 24s
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 24s
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { ArticleMeta } from './ArticleCard.vue'
|
||||
import type { ToolPage } from '~/utils/routes'
|
||||
|
||||
/* Centered search popup: the sidebar and home search bars (or Ctrl/⌘+K,
|
||||
or "/" anywhere but a text field) open it, then it live-searches
|
||||
articles against the API as you type. ↑/↓ select, ↵ opens, esc closes. */
|
||||
articles against the API as you type. ↑/↓ select, ↵ opens, esc closes.
|
||||
The wiki's own pages — the link map and the graphing calculator — answer
|
||||
the search too, and ↵ on one lays that surface on the book beside the
|
||||
open articles rather than turning the page away to it. */
|
||||
const api = `${useRuntimeConfig().public.apiBase}/api`
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -13,9 +17,20 @@ const { open, show, hide, toggle } = useSearch()
|
||||
const { hide: hideSettings } = useSettings()
|
||||
const { hide: hideProfile } = useProfileMenu()
|
||||
const { viewed } = useRecentlyViewed()
|
||||
const { openTool } = useArticleTabs()
|
||||
|
||||
const MAX_RESULTS = 8
|
||||
|
||||
/* the wiki's own pages join the search under their names and a few plain
|
||||
words for what they are; each answers at its own address */
|
||||
const PAGE_TOOLS: { tool: ToolPage; terms: string[] }[] = [
|
||||
{ tool: 'graph', terms: ['graph', 'map', 'link map', 'network', 'constellation'] },
|
||||
{
|
||||
tool: 'calculator',
|
||||
terms: ['calculator', 'calc', 'plot', 'plotter', 'graphing', 'plane'],
|
||||
},
|
||||
]
|
||||
|
||||
const q = ref('')
|
||||
const results = ref<ArticleMeta[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -24,6 +39,17 @@ const activeIndex = ref(0)
|
||||
|
||||
const trimmed = computed(() => q.value.trim())
|
||||
|
||||
/* which of the wiki's own pages the term names — matched against the words
|
||||
and the page's own name, either reading into the other, so "calc" leads
|
||||
to the Calculator as surely as the whole word does */
|
||||
const pageRows = computed(() => {
|
||||
const term = trimmed.value.toLowerCase()
|
||||
if (!term) return []
|
||||
return PAGE_TOOLS.filter(({ tool, terms }) =>
|
||||
terms.some((w) => w.includes(term) || term.includes(w)) || tool.includes(term)
|
||||
)
|
||||
})
|
||||
|
||||
/* --- focus: the field takes focus on open, focus returns on close --- */
|
||||
const inputEl = ref<HTMLInputElement | null>(null)
|
||||
let previouslyFocused: HTMLElement | null = null
|
||||
@@ -122,8 +148,19 @@ function openAllResults() {
|
||||
void router.push({ path: '/', query: { q: trimmed.value } })
|
||||
}
|
||||
|
||||
/* open one of the wiki's own pages: lay the page on the book first, so it
|
||||
arrives with the surface already holding it, then turn the desk to its
|
||||
address — where the page is already open, this simply turns back to it */
|
||||
function openPageTool(tool: ToolPage) {
|
||||
hide()
|
||||
openTool(tool)
|
||||
void router.push(toolHref(tool))
|
||||
}
|
||||
|
||||
/* ↑/↓ walk the page rows and the articles as one list — the wiki's own
|
||||
pages lead when they match */
|
||||
function onFieldKeydown(e: KeyboardEvent) {
|
||||
const n = results.value.length
|
||||
const n = pageRows.value.length + results.value.length
|
||||
if (e.key === 'ArrowDown' && n) {
|
||||
e.preventDefault()
|
||||
activeIndex.value = (activeIndex.value + 1) % n
|
||||
@@ -132,7 +169,12 @@ function onFieldKeydown(e: KeyboardEvent) {
|
||||
activeIndex.value = (activeIndex.value - 1 + n) % n
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
const chosen = results.value[activeIndex.value] ?? results.value[0]
|
||||
const page = pageRows.value[activeIndex.value]
|
||||
if (page) {
|
||||
openPageTool(page.tool)
|
||||
return
|
||||
}
|
||||
const chosen = results.value[activeIndex.value - pageRows.value.length] ?? results.value[0]
|
||||
if (chosen) openArticle(chosen)
|
||||
else if (trimmed.value) openAllResults()
|
||||
}
|
||||
@@ -209,18 +251,52 @@ function highlight(text: string): Part[] {
|
||||
</div>
|
||||
|
||||
<div class="search-modal-body">
|
||||
<div v-if="failed" class="banner">
|
||||
<span class="glyph">!</span>
|
||||
<span>{{ t('search.apiDown') }}</span>
|
||||
</div>
|
||||
|
||||
<ul v-else-if="results.length" class="search-results" role="listbox">
|
||||
<li v-for="(a, i) in results" :key="a._id" role="option" :aria-selected="i === activeIndex">
|
||||
<!-- one list, arrowed as one: the wiki's own pages lead when the
|
||||
term names them, the articles answer under them -->
|
||||
<ul v-if="pageRows.length || results.length" class="search-results" role="listbox">
|
||||
<li v-for="(p, i) in pageRows" :key="p.tool" role="option" :aria-selected="i === activeIndex">
|
||||
<NuxtLink
|
||||
:to="articleHref(a)"
|
||||
:to="toolHref(p.tool)"
|
||||
class="search-result"
|
||||
:class="{ active: i === activeIndex }"
|
||||
@mouseenter="activeIndex = i"
|
||||
@click.prevent="openPageTool(p.tool)"
|
||||
>
|
||||
<span class="sr-title">
|
||||
<span class="sr-page-glyph" aria-hidden="true">
|
||||
<svg v-if="p.tool === 'graph'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="18" cy="5" r="3" />
|
||||
<circle cx="6" cy="12" r="3" />
|
||||
<circle cx="18" cy="19" r="3" />
|
||||
<path d="m8.6 10.5 6.8-4M8.6 13.5l6.8 4" />
|
||||
</svg>
|
||||
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 3v17a1 1 0 0 0 1 1h17" />
|
||||
<path d="M4.5 16.5c2.6 0 3.4-10.5 6.8-10.5s3.9 7 8.2 7" />
|
||||
</svg>
|
||||
</span>
|
||||
<template v-for="(p2, pi) in highlight(t(TOOL_LABEL_KEYS[p.tool]))" :key="pi">
|
||||
<mark v-if="p2.hit">{{ p2.text }}</mark>
|
||||
<template v-else>{{ p2.text }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<span class="sr-meta">
|
||||
<span>{{ toolHref(p.tool) }}</span>
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-for="(a, i) in results"
|
||||
:key="a._id"
|
||||
role="option"
|
||||
:aria-selected="i + pageRows.length === activeIndex"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="articleHref(a)"
|
||||
class="search-result"
|
||||
:class="{ active: i + pageRows.length === activeIndex }"
|
||||
@mouseenter="activeIndex = i + pageRows.length"
|
||||
@click.prevent="openArticle(a)"
|
||||
>
|
||||
<span class="sr-title" :lang="articleLanguage(a)">
|
||||
@@ -245,6 +321,11 @@ function highlight(text: string): Part[] {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-else-if="failed" class="banner">
|
||||
<span class="glyph">!</span>
|
||||
<span>{{ t('search.apiDown') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="loading">
|
||||
<div v-for="i in 3" :key="i" class="search-skeleton" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user