
* wip
* wip
* wip
* test
* wip rollup pluginでsearchIndexの情報生成
* wip
* SPDX
* wip: markerIdを自動付与
* rollupでビルド時・devモード時に毎回uuidを生成するように
* 開発サーバーでだけ必要な挙動は開発サーバーのみで
* 条件が逆
* wip: childrenの生成
* update comment
* update comment
* rename auto generated file
* hashをパスと行数から決定
* Update privacy.vue
* Update privacy.vue
* wip
* Update general.vue
* Update general.vue
* wip
* wip
* Update SearchMarker.vue
* wip
* Update profile.vue
* Update mute-block.vue
* Update mute-block.vue
* Update general.vue
* Update general.vue
* childrenがduplicate key errorを吐く問題をいったん解決
* マーカーの形を成形
* loggerを置きかえ
* とりあえず省略記法に対応
* Refactor and Format codes
* wip
* Update settings-search-index.ts
* wip
* wip
* とりあえず不確定要因の仮置きidを削除
* hashの生成を正規化(絶対パスになっていたのを緩和)
* pathの入力を省略可能に
* adminでもパス生成できるように
* Update settings-search-index.ts
* Update privacy.vue
* wip
* build searchIndex
* wip
* build
* Update general.vue
* build
* Update sounds.vue
* build
* build
* Update sounds.vue
* 🎨
* 🎨
* Update privacy.vue
* Update privacy.vue
* Update security.vue
* create-search-indexを多少改善
* build
* Update 2fa.vue
* wip
* 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義
* キャッシュはdevServerでなくても更新
* Revert "wip"
This reverts commit 41bffd3a13
.
* inlining
* wip
* Update theme.vue
* 🎨
* wip normalize
* Update theme.vue
* キャッシュのパス変換
* build
* wip
* wip
* Update SearchMarker.vue
* i18n.ts['key'] の形式が取り出せない問題のFix
* build
* 仮でpath入れ
* 必ず絶対パスが使われるように
* wip
* 🎨
* storybookビルド時はcreateSearchIndexをしない
* inliningの構造化
* format code
* Update index.vue
* wip
* wip
* 🎨
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* clean up
* Update navbar.vue
* enhance: 検索で上下矢印を使用することで検索結果を移動できるように
* refactor
* fix(frontend): PageWindowでSearchMarkerが動作するように
* enhance(frontend): SearchMarkerの点滅を一定時間で止める
* lint fix
* fix: 子要素監視が抜けていたのを修正
* アニメーションの回数はCSSで制御するように
* refactor
* enhance(frontend): 検索インデックス作成時のログを削減
* revert
* fix
* fix
---------
Co-authored-by: tai-cha <dev@taichan.site>
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
357 lines
8.0 KiB
Vue
357 lines
8.0 KiB
Vue
<!--
|
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
-->
|
|
|
|
<template>
|
|
<div ref="rootEl" class="rrevdjwu" :class="{ grid }">
|
|
<MkInput
|
|
v-model="search"
|
|
:placeholder="i18n.ts.search"
|
|
type="search"
|
|
style="margin-bottom: 16px;"
|
|
@keydown="searchOnKeyDown"
|
|
>
|
|
<template #prefix><i class="ti ti-search"></i></template>
|
|
</MkInput>
|
|
|
|
<template v-if="search == ''">
|
|
<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>
|
|
</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>
|
|
|
|
<script lang="ts">
|
|
export type SuperMenuDef = {
|
|
title?: string;
|
|
items: ({
|
|
type: 'a';
|
|
href: string;
|
|
target?: string;
|
|
icon?: string;
|
|
text: string;
|
|
danger?: boolean;
|
|
active?: boolean;
|
|
} | {
|
|
type: 'button';
|
|
icon?: string;
|
|
text: string;
|
|
danger?: boolean;
|
|
active?: boolean;
|
|
action: (ev: MouseEvent) => void | Promise<void>;
|
|
} | {
|
|
type?: 'link';
|
|
to: string;
|
|
icon?: string;
|
|
text: string;
|
|
danger?: boolean;
|
|
active?: boolean;
|
|
})[];
|
|
};
|
|
</script>
|
|
|
|
<script lang="ts" setup>
|
|
import { useTemplateRef, ref, watch, nextTick } from 'vue';
|
|
import type { SearchIndexItem } from '@/scripts/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/supplier.js';
|
|
|
|
const props = defineProps<{
|
|
def: SuperMenuDef[];
|
|
grid?: boolean;
|
|
searchIndex: SearchIndexItem[];
|
|
}>();
|
|
|
|
const router = useRouter();
|
|
const rootEl = useTemplateRef('rootEl');
|
|
|
|
const search = ref('');
|
|
const searchSelectedIndex = ref<null | number>(null);
|
|
const searchResult = ref<{
|
|
id: string;
|
|
path: string;
|
|
label: string;
|
|
icon?: string;
|
|
isRoot: boolean;
|
|
parentLabels: string[];
|
|
}[]>([]);
|
|
|
|
watch(search, (value) => {
|
|
searchResult.value = [];
|
|
searchSelectedIndex.value = null;
|
|
|
|
if (value === '') {
|
|
return;
|
|
}
|
|
|
|
const dive = (items: SearchIndexItem[], parents: SearchIndexItem[] = []) => {
|
|
for (const item of items) {
|
|
const matched =
|
|
item.label.includes(value.toLowerCase()) ||
|
|
item.keywords.some((x) => x.toLowerCase().includes(value.toLowerCase()));
|
|
|
|
if (matched) {
|
|
searchResult.value.push({
|
|
id: item.id,
|
|
path: item.path ?? parents.find((x) => x.path != null)?.path,
|
|
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]);
|
|
}
|
|
}
|
|
};
|
|
|
|
dive(props.searchIndex);
|
|
});
|
|
|
|
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>
|
|
.rrevdjwu {
|
|
> .group {
|
|
& + .group {
|
|
margin-top: 16px;
|
|
padding-top: 16px;
|
|
border-top: solid 0.5px var(--MI_THEME-divider);
|
|
}
|
|
|
|
> .title {
|
|
opacity: 0.7;
|
|
margin: 0 0 8px 0;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
> .items {
|
|
> .item {
|
|
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);
|
|
}
|
|
|
|
&:focus-visible {
|
|
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;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
&.grid {
|
|
> .group {
|
|
margin-left: 0;
|
|
margin-right: 0;
|
|
|
|
& + .group {
|
|
padding-top: 0;
|
|
border-top: none;
|
|
}
|
|
|
|
> .title {
|
|
font-size: 1em;
|
|
opacity: 0.7;
|
|
margin: 0 0 8px 16px;
|
|
}
|
|
|
|
> .items {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
|
|
grid-gap: 16px;
|
|
padding: 0 16px;
|
|
|
|
> .item {
|
|
flex-direction: column;
|
|
text-align: center;
|
|
padding: 0;
|
|
|
|
&:hover {
|
|
text-decoration: none;
|
|
background: none;
|
|
color: var(--MI_THEME-accent);
|
|
|
|
> .icon {
|
|
background: var(--MI_THEME-accentedBg);
|
|
}
|
|
}
|
|
|
|
> .icon {
|
|
display: grid;
|
|
place-content: center;
|
|
margin-right: 0;
|
|
margin-bottom: 6px;
|
|
font-size: 1.5em;
|
|
width: 60px;
|
|
height: 60px;
|
|
aspect-ratio: 1;
|
|
background: var(--MI_THEME-panel);
|
|
border-radius: 100%;
|
|
}
|
|
|
|
> .text {
|
|
padding-right: 0;
|
|
width: 100%;
|
|
font-size: 0.8em;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.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>
|