summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-02-27 09:32:39 +0900
committerGitHub <noreply@github.com>2025-02-27 00:32:39 +0000
commitec8381522789a7fc4329a627770d80a0cd7774a4 (patch)
treea4bf4db4bae51fa4020def0fea19b08154eaf563 /packages/frontend/src
parentUpdate CHANGELOG.md (diff)
downloadsharkey-ec8381522789a7fc4329a627770d80a0cd7774a4.tar.gz
sharkey-ec8381522789a7fc4329a627770d80a0cd7774a4.tar.bz2
sharkey-ec8381522789a7fc4329a627770d80a0cd7774a4.zip
fix(frontend): MkSelectの初期値が表示されない場合がある (#15559)
* fix * Update CHANGELOG.md
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkSelect.vue130
-rw-r--r--packages/frontend/src/pages/settings/theme.vue83
2 files changed, 167 insertions, 46 deletions
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 83a79d0c9f..15717802ec 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -41,11 +41,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
-import type { VNode, VNodeChild } from 'vue';
import { useInterval } from '@@/js/use-interval.js';
+import type { VNode, VNodeChild } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
+type ItemOption = {
+ type?: 'option';
+ value: string | number | null;
+ label: string;
+};
+
+type ItemGroup = {
+ type: 'group';
+ label: string;
+ items: ItemOption[];
+};
+
+export type MkSelectItem = ItemOption | ItemGroup;
+
+// TODO: itemsをslot内のoptionで指定する用法は廃止する(props.itemsを必須化する)
+// see: https://github.com/misskey-dev/misskey/issues/15558
+
const props = defineProps<{
modelValue: string | number | null;
required?: boolean;
@@ -56,6 +73,7 @@ const props = defineProps<{
inline?: boolean;
small?: boolean;
large?: boolean;
+ items?: MkSelectItem[];
}>();
const emit = defineEmits<{
@@ -107,7 +125,30 @@ onMounted(() => {
});
});
-watch(modelValue, () => {
+watch([modelValue, () => props.items], () => {
+ if (props.items) {
+ let found: ItemOption | null = null;
+ for (const item of props.items) {
+ if (item.type === 'group') {
+ for (const option of item.items) {
+ if (option.value === modelValue.value) {
+ found = option;
+ break;
+ }
+ }
+ } else {
+ if (item.value === modelValue.value) {
+ found = item;
+ break;
+ }
+ }
+ }
+ if (found) {
+ currentValueText.value = found.label;
+ }
+ return;
+ }
+
const scanOptions = (options: VNodeChild[]) => {
for (const vnode of options) {
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
@@ -130,7 +171,7 @@ watch(modelValue, () => {
};
scanOptions(slots.default!());
-}, { immediate: true });
+}, { immediate: true, deep: true });
function show() {
if (opening.value) return;
@@ -139,41 +180,70 @@ function show() {
opening.value = true;
const menu: MenuItem[] = [];
- let options = slots.default!();
- const pushOption = (option: VNode) => {
- menu.push({
- text: option.children as string,
- active: computed(() => modelValue.value === option.props?.value),
- action: () => {
- emit('update:modelValue', option.props?.value);
- },
- });
- };
-
- const scanOptions = (options: VNodeChild[]) => {
- for (const vnode of options) {
- if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
- if (vnode.type === 'optgroup') {
- const optgroup = vnode;
+ if (props.items) {
+ for (const item of props.items) {
+ if (item.type === 'group') {
menu.push({
type: 'label',
- text: optgroup.props?.label,
+ text: item.label,
});
- if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
- } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
- const fragment = vnode;
- if (Array.isArray(fragment.children)) scanOptions(fragment.children);
- } else if (vnode.props == null) { // v-if で条件が false のときにこうなる
- // nop?
+ for (const option of item.items) {
+ menu.push({
+ text: option.label,
+ active: computed(() => modelValue.value === option.value),
+ action: () => {
+ emit('update:modelValue', option.value);
+ },
+ });
+ }
} else {
- const option = vnode;
- pushOption(option);
+ menu.push({
+ text: item.label,
+ active: computed(() => modelValue.value === item.value),
+ action: () => {
+ emit('update:modelValue', item.value);
+ },
+ });
}
}
- };
+ } else {
+ let options = slots.default!();
+
+ const pushOption = (option: VNode) => {
+ menu.push({
+ text: option.children as string,
+ active: computed(() => modelValue.value === option.props?.value),
+ action: () => {
+ emit('update:modelValue', option.props?.value);
+ },
+ });
+ };
- scanOptions(options);
+ const scanOptions = (options: VNodeChild[]) => {
+ for (const vnode of options) {
+ if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
+ if (vnode.type === 'optgroup') {
+ const optgroup = vnode;
+ menu.push({
+ type: 'label',
+ text: optgroup.props?.label,
+ });
+ if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
+ } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
+ const fragment = vnode;
+ if (Array.isArray(fragment.children)) scanOptions(fragment.children);
+ } else if (vnode.props == null) { // v-if で条件が false のときにこうなる
+ // nop?
+ } else {
+ const option = vnode;
+ pushOption(option);
+ }
+ }
+ };
+
+ scanOptions(options);
+ }
os.popupMenu(menu, container.value, {
width: container.value?.offsetWidth,
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index f1ec231588..fcf5b3cd9b 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -32,27 +32,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="selects">
- <MkSelect v-model="lightThemeId" large class="select">
+ <MkSelect v-model="lightThemeId" large class="select" :items="lightThemeSelectorItems">
<template #label>{{ i18n.ts.themeForLightMode }}</template>
<template #prefix><i class="ti ti-sun"></i></template>
- <option v-if="instanceLightTheme" :key="'instance:' + instanceLightTheme.id" :value="instanceLightTheme.id">{{ instanceLightTheme.name }}</option>
- <optgroup v-if="installedLightThemes.length > 0" :label="i18n.ts._theme.installedThemes">
- <option v-for="x in installedLightThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
- <optgroup :label="i18n.ts._theme.builtinThemes">
- <option v-for="x in builtinLightThemes" :key="'builtin:' + x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
</MkSelect>
- <MkSelect v-model="darkThemeId" large class="select">
+ <MkSelect v-model="darkThemeId" large class="select" :items="darkThemeSelectorItems">
<template #label>{{ i18n.ts.themeForDarkMode }}</template>
<template #prefix><i class="ti ti-moon"></i></template>
- <option v-if="instanceDarkTheme" :key="'instance:' + instanceDarkTheme.id" :value="instanceDarkTheme.id">{{ instanceDarkTheme.name }}</option>
- <optgroup v-if="installedDarkThemes.length > 0" :label="i18n.ts._theme.installedThemes">
- <option v-for="x in installedDarkThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
- <optgroup :label="i18n.ts._theme.builtinThemes">
- <option v-for="x in builtinDarkThemes" :key="'builtin:' + x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
</MkSelect>
</div>
@@ -73,6 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, onActivated, ref, watch } from 'vue';
import JSON5 from 'json5';
+import type { MkSelectItem } from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
@@ -102,6 +89,70 @@ const installedLightThemes = computed(() => installedThemes.value.filter(t => t.
const builtinLightThemes = computed(() => builtinThemes.value.filter(t => t.base === 'light' || t.kind === 'light'));
const themes = computed(() => uniqueBy([instanceDarkTheme.value, instanceLightTheme.value, ...builtinThemes.value, ...installedThemes.value].filter(x => x != null), theme => theme.id));
+const lightThemeSelectorItems = computed(() => {
+ const items = [] as MkSelectItem[];
+ if (instanceLightTheme.value) {
+ items.push({
+ type: 'option',
+ value: instanceLightTheme.value.id,
+ label: instanceLightTheme.value.name,
+ });
+ }
+ if (installedLightThemes.value.length > 0) {
+ items.push({
+ type: 'group',
+ label: i18n.ts._theme.installedThemes,
+ items: installedLightThemes.value.map(x => ({
+ type: 'option',
+ value: x.id,
+ label: x.name,
+ })),
+ });
+ }
+ items.push({
+ type: 'group',
+ label: i18n.ts._theme.builtinThemes,
+ items: builtinLightThemes.value.map(x => ({
+ type: 'option',
+ value: x.id,
+ label: x.name,
+ })),
+ });
+ return items;
+});
+
+const darkThemeSelectorItems = computed(() => {
+ const items = [] as MkSelectItem[];
+ if (instanceDarkTheme.value) {
+ items.push({
+ type: 'option',
+ value: instanceDarkTheme.value.id,
+ label: instanceDarkTheme.value.name,
+ });
+ }
+ if (installedDarkThemes.value.length > 0) {
+ items.push({
+ type: 'group',
+ label: i18n.ts._theme.installedThemes,
+ items: installedDarkThemes.value.map(x => ({
+ type: 'option',
+ value: x.id,
+ label: x.name,
+ })),
+ });
+ }
+ items.push({
+ type: 'group',
+ label: i18n.ts._theme.builtinThemes,
+ items: builtinDarkThemes.value.map(x => ({
+ type: 'option',
+ value: x.id,
+ label: x.name,
+ })),
+ });
+ return items;
+});
+
const darkTheme = ColdDeviceStorage.ref('darkTheme');
const darkThemeId = computed({
get() {