merge upstream
This commit is contained in:
commit
d8908ef2d8
1065 changed files with 32953 additions and 20092 deletions
|
|
@ -4,27 +4,62 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="rrevdjwu" :class="{ grid }">
|
||||
<div v-for="group in def" class="group">
|
||||
<div v-if="group.title" class="title">{{ group.title }}</div>
|
||||
<div ref="rootEl" class="rrevdjwu" :class="{ grid }">
|
||||
<MkInput
|
||||
v-if="searchIndex && searchIndex.length > 0"
|
||||
v-model="searchQuery"
|
||||
:placeholder="i18n.ts.search"
|
||||
type="search"
|
||||
style="margin-bottom: 16px;"
|
||||
@input.passive="searchOnInput"
|
||||
@keydown="searchOnKeyDown"
|
||||
>
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<div class="items">
|
||||
<template v-for="(item, i) in group.items">
|
||||
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</button>
|
||||
<MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</MkA>
|
||||
</template>
|
||||
<template v-if="rawSearchQuery == ''">
|
||||
<div v-for="group in def" class="group">
|
||||
<div v-if="group.title" class="title">{{ group.title }}</div>
|
||||
|
||||
<div class="items">
|
||||
<template v-for="(item, i) in group.items">
|
||||
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</button>
|
||||
<MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">{{ item.text }}</span>
|
||||
</MkA>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="item, index in searchResult">
|
||||
<MkA
|
||||
:to="item.path + '#' + item.id"
|
||||
class="_button searchResultItem"
|
||||
:class="{ selected: searchSelectedIndex !== null && searchSelectedIndex === index }"
|
||||
>
|
||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||
<span class="text">
|
||||
<template v-if="item.isRoot">
|
||||
{{ item.label }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span style="opacity: 0.7; font-size: 90%;">{{ item.parentLabels.join(' > ') }}</span>
|
||||
<br>
|
||||
<span>{{ item.label }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</MkA>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -45,7 +80,7 @@ export type SuperMenuDef = {
|
|||
text: string;
|
||||
danger?: boolean;
|
||||
active?: boolean;
|
||||
action: (ev: MouseEvent) => void;
|
||||
action: (ev: MouseEvent) => void | Promise<void>;
|
||||
} | {
|
||||
type?: 'link';
|
||||
to: string;
|
||||
|
|
@ -58,10 +93,115 @@ export type SuperMenuDef = {
|
|||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
import { useTemplateRef, ref, watch, nextTick } from 'vue';
|
||||
import type { SearchIndexItem } from '@/utility/autogen/settings-search-index.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { initIntlString, compareStringIncludes } from '@/utility/intl-string.js';
|
||||
|
||||
const props = defineProps<{
|
||||
def: SuperMenuDef[];
|
||||
grid?: boolean;
|
||||
searchIndex?: SearchIndexItem[];
|
||||
}>();
|
||||
|
||||
initIntlString();
|
||||
|
||||
const router = useRouter();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
|
||||
const searchQuery = ref('');
|
||||
const rawSearchQuery = ref('');
|
||||
|
||||
const searchSelectedIndex = ref<null | number>(null);
|
||||
const searchResult = ref<{
|
||||
id: string;
|
||||
path: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
isRoot: boolean;
|
||||
parentLabels: string[];
|
||||
}[]>([]);
|
||||
|
||||
watch(searchQuery, (value) => {
|
||||
rawSearchQuery.value = value;
|
||||
});
|
||||
|
||||
watch(rawSearchQuery, (value) => {
|
||||
searchResult.value = [];
|
||||
searchSelectedIndex.value = null;
|
||||
|
||||
if (value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const dive = (items: SearchIndexItem[], parents: SearchIndexItem[] = []) => {
|
||||
for (const item of items) {
|
||||
const matched = (
|
||||
compareStringIncludes(item.label, value) ||
|
||||
item.keywords.some((x) => compareStringIncludes(x, value))
|
||||
);
|
||||
|
||||
if (matched) {
|
||||
searchResult.value.push({
|
||||
id: item.id,
|
||||
path: item.path ?? parents.find((x) => x.path != null)?.path ?? '/', // never gets `/`
|
||||
label: item.label,
|
||||
parentLabels: parents.map((x) => x.label).toReversed(),
|
||||
icon: item.icon ?? parents.find((x) => x.icon != null)?.icon,
|
||||
isRoot: parents.length === 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
dive(item.children, [item, ...parents]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (props.searchIndex) {
|
||||
dive(props.searchIndex);
|
||||
}
|
||||
});
|
||||
|
||||
function searchOnInput(ev: InputEvent) {
|
||||
searchSelectedIndex.value = null;
|
||||
rawSearchQuery.value = (ev.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
function searchOnKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.isComposing) return;
|
||||
|
||||
if (ev.key === 'Enter' && searchSelectedIndex.value != null) {
|
||||
ev.preventDefault();
|
||||
router.push(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id);
|
||||
} else if (ev.key === 'ArrowDown') {
|
||||
ev.preventDefault();
|
||||
const current = searchSelectedIndex.value ?? -1;
|
||||
searchSelectedIndex.value = current + 1 >= searchResult.value.length ? 0 : current + 1;
|
||||
} else if (ev.key === 'ArrowUp') {
|
||||
ev.preventDefault();
|
||||
const current = searchSelectedIndex.value ?? 0;
|
||||
searchSelectedIndex.value = current - 1 < 0 ? searchResult.value.length - 1 : current - 1;
|
||||
}
|
||||
|
||||
if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
|
||||
nextTick(() => {
|
||||
if (!rootEl.value) return;
|
||||
const selectedEl = rootEl.value.querySelector<HTMLElement>('.searchResultItem.selected');
|
||||
if (selectedEl != null) {
|
||||
const scrollContainer = getScrollContainer(selectedEl);
|
||||
if (!scrollContainer) return;
|
||||
scrollContainer.scrollTo({
|
||||
top: selectedEl.offsetTop - scrollContainer.clientHeight / 2 + selectedEl.clientHeight / 2,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
@ -184,5 +324,52 @@ defineProps<{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchResultItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 16px 9px 8px;
|
||||
border-radius: 9px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: var(--MI_THEME-panelHighlight);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
outline: 2px solid var(--MI_THEME-focus);
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
&.selected {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--MI_THEME-accent);
|
||||
background: var(--MI_THEME-accentedBg);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: var(--MI_THEME-error);
|
||||
}
|
||||
|
||||
> .icon {
|
||||
width: 32px;
|
||||
margin-right: 2px;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
> .text {
|
||||
white-space: normal;
|
||||
padding-right: 12px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue