enhance(frontend): improve usability on touch device

This commit is contained in:
syuilo 2025-03-16 10:58:06 +09:00
parent 2ddedd0ce6
commit c2940fd77c
15 changed files with 69 additions and 17 deletions

8
locales/index.d.ts vendored
View File

@ -5433,6 +5433,14 @@ export interface Locale extends ILocale {
* *
*/ */
"timelineAndNote": string; "timelineAndNote": string;
/**
*
*/
"makeEveryTextElementsSelectable": string;
/**
*
*/
"makeEveryTextElementsSelectable_description": string;
}; };
"_preferencesProfile": { "_preferencesProfile": {
/** /**

View File

@ -1357,6 +1357,8 @@ _settings:
appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。" appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。"
soundsBanner: "クライアントで再生するサウンドの設定が行えます。" soundsBanner: "クライアントで再生するサウンドの設定が行えます。"
timelineAndNote: "タイムラインとノート" timelineAndNote: "タイムラインとノート"
makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする"
makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。"
_preferencesProfile: _preferencesProfile:
profileName: "プロファイル名" profileName: "プロファイル名"

View File

@ -234,6 +234,10 @@ export async function common(createVue: () => App<Element>) {
}); });
} }
if (prefer.s.makeEveryTextElementsSelectable) {
document.documentElement.classList.add('forceSelectableAll');
}
//#region Fetch user //#region Fetch user
if ($i && $i.token) { if ($i && $i.token) {
if (_DEV_) { if (_DEV_) {

View File

@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i> <i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i>
<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/> <MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/>
</div> </div>
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header> <header v-if="title" :class="$style.title" class="_selectable"><Mfm :text="title"/></header>
<div v-if="text" :class="$style.text"><Mfm :text="text"/></div> <div v-if="text" :class="$style.text" class="_selectable"><Mfm :text="text"/></div>
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown"> <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template> <template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
<template #caption> <template #caption>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="[$style.root, { [$style.warn]: warn }]"> <div :class="[$style.root, { [$style.warn]: warn }]" class="_selectable">
<i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i> <i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i>
<i v-else class="ti ti-info-circle" :class="$style.i"></i> <i v-else class="ti ti-info-circle" :class="$style.i"></i>
<div><slot></slot></div> <div><slot></slot></div>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div> <div class="_selectable">
<div :class="$style.label" @click="focus"><slot name="label"></slot></div> <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
<div :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]"> <div :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]">
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div> <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
@ -45,13 +45,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
import type { InputHTMLAttributes } from 'vue';
import { debounce } from 'throttle-debounce'; import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import type { InputHTMLAttributes } from 'vue';
import type { SuggestionType } from '@/utility/autocomplete.js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { Autocomplete } from '@/utility/autocomplete.js'; import { Autocomplete } from '@/utility/autocomplete.js';
import type { SuggestionType } from '@/utility/autocomplete.js';
const props = defineProps<{ const props = defineProps<{
modelValue: string | number | null; modelValue: string | number | null;

View File

@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.items"> <div :class="$style.items">
<div> <div>
<div :class="$style.label">{{ i18n.ts.invitationCode }}</div> <div :class="$style.label">{{ i18n.ts.invitationCode }}</div>
<div>{{ invite.code }}</div> <div class="_selectableAtomic">{{ invite.code }}</div>
</div> </div>
<div v-if="moderator"> <div v-if="moderator">
<div :class="$style.label">{{ i18n.ts.inviteCodeCreator }}</div> <div :class="$style.label">{{ i18n.ts.inviteCodeCreator }}</div>

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.key"> <div :class="$style.key">
<slot name="key"></slot> <slot name="key"></slot>
</div> </div>
<div :class="$style.value"> <div :class="$style.value" class="_selectable">
<slot name="value"></slot> <slot name="value"></slot>
<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button> <button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button>
</div> </div>

View File

@ -76,12 +76,13 @@ SPDX-License-Identifier: AGPL-3.0-only
:emojiUrls="appearNote.emojis" :emojiUrls="appearNote.emojis"
:enableEmojiMenu="true" :enableEmojiMenu="true"
:enableEmojiMenuReaction="true" :enableEmojiMenuReaction="true"
class="_selectable"
/> />
<div v-if="translating || translation" :class="$style.translation"> <div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else-if="translation"> <div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" class="_selectable"/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -97,13 +97,14 @@ SPDX-License-Identifier: AGPL-3.0-only
:emojiUrls="appearNote.emojis" :emojiUrls="appearNote.emojis"
:enableEmojiMenu="true" :enableEmojiMenu="true"
:enableEmojiMenuReaction="true" :enableEmojiMenuReaction="true"
class="_selectable"
/> />
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
<div v-if="translating || translation" :class="$style.translation"> <div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else-if="translation"> <div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" class="_selectable"/>
</div> </div>
</div> </div>
<div v-if="appearNote.files && appearNote.files.length > 0"> <div v-if="appearNote.files && appearNote.files.length > 0">

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div> <div class="_selectable">
<div :class="$style.label" @click="focus"><slot name="label"></slot></div> <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
<div :class="{ [$style.disabled]: disabled, [$style.focused]: focused, [$style.tall]: tall, [$style.pre]: pre }" style="position: relative;"> <div :class="{ [$style.disabled]: disabled, [$style.focused]: focused, [$style.tall]: tall, [$style.pre]: pre }" style="position: relative;">
<textarea <textarea
@ -38,10 +38,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue'; import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
import { debounce } from 'throttle-debounce'; import { debounce } from 'throttle-debounce';
import type { SuggestionType } from '@/utility/autocomplete.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { Autocomplete } from '@/utility/autocomplete.js'; import { Autocomplete } from '@/utility/autocomplete.js';
import type { SuggestionType } from '@/utility/autocomplete.js';
const props = defineProps<{ const props = defineProps<{
modelValue: string | null; modelValue: string | null;

View File

@ -58,6 +58,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch> </MkSwitch>
</MkPreferenceContainer> </MkPreferenceContainer>
</SearchMarker> </SearchMarker>
<SearchMarker :keywords="['text', 'selectable']">
<MkPreferenceContainer k="makeEveryTextElementsSelectable">
<MkSwitch v-model="makeEveryTextElementsSelectable">
<template #label><SearchLabel>{{ i18n.ts._settings.makeEveryTextElementsSelectable }}</SearchLabel></template>
<template #caption>{{ i18n.ts._settings.makeEveryTextElementsSelectable_description }}</template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
</div> </div>
<SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']"> <SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']">
@ -122,6 +131,7 @@ const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer'); const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
const contextMenu = prefer.model('contextMenu'); const contextMenu = prefer.model('contextMenu');
const menuStyle = prefer.model('menuStyle'); const menuStyle = prefer.model('menuStyle');
const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable');
const fontSize = ref(miLocalStorage.getItem('fontSize')); const fontSize = ref(miLocalStorage.getItem('fontSize'));
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null); const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
@ -147,6 +157,7 @@ watch([
contextMenu, contextMenu,
fontSize, fontSize,
useSystemFont, useSystemFont,
makeEveryTextElementsSelectable,
], async () => { ], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
}); });

View File

@ -313,6 +313,9 @@ export const PREF_DEF = {
defaultFollowWithReplies: { defaultFollowWithReplies: {
default: false, default: false,
}, },
makeEveryTextElementsSelectable: {
default: DEFAULT_DEVICE_KIND === 'desktop',
},
plugins: { plugins: {
default: [] as Plugin[], default: [] as Plugin[],
}, },

View File

@ -81,6 +81,11 @@ html {
&.useSystemFont { &.useSystemFont {
font-family: system-ui; font-family: system-ui;
} }
&:not(.forceSelectableAll) {
user-select: none;
-webkit-user-select: none;
}
} }
html._themeChanging_ { html._themeChanging_ {
@ -120,6 +125,8 @@ a {
textarea, input { textarea, input {
tap-highlight-color: transparent; tap-highlight-color: transparent;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
user-select: text;
-webkit-user-select: text;
} }
optgroup, option { optgroup, option {
@ -184,6 +191,16 @@ rt {
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
} }
._selectable {
user-select: text;
-webkit-user-select: text;
}
._selectableAtomic {
user-select: all;
-webkit-user-select: all;
}
._noSelect { ._noSelect {
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;

View File

@ -897,22 +897,27 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['native', 'system', 'video', 'audio', 'player', 'media'], keywords: ['native', 'system', 'video', 'audio', 'player', 'media'],
}, },
{ {
id: '1fV9WINCQ', id: 'b1GYEEJeh',
label: i18n.ts._settings.makeEveryTextElementsSelectable,
keywords: ['text', 'selectable'],
},
{
id: 'vVLxwINTJ',
label: i18n.ts.menuStyle, label: i18n.ts.menuStyle,
keywords: ['menu', 'style', 'popup', 'drawer'], keywords: ['menu', 'style', 'popup', 'drawer'],
}, },
{ {
id: 'mLQzlKUNu', id: '14cMhMLHL',
label: i18n.ts._contextMenu.title, label: i18n.ts._contextMenu.title,
keywords: ['contextmenu', 'system', 'native'], keywords: ['contextmenu', 'system', 'native'],
}, },
{ {
id: 'yP96aA3j9', id: 'oSo4LXMX9',
label: i18n.ts.fontSize, label: i18n.ts.fontSize,
keywords: ['font', 'size'], keywords: ['font', 'size'],
}, },
{ {
id: 'jQeiMopFE', id: '7LQSAThST',
label: i18n.ts.useSystemFont, label: i18n.ts.useSystemFont,
keywords: ['font', 'system', 'native'], keywords: ['font', 'system', 'native'],
}, },