feat(frontend): 投稿ウインドウにMFM要素を追加するボタンの追加 (#12788)
* functionPicker の追加 * Update CHANGELOG.md * fix lint errors * Add addMfmFunction * add enableQuickAddMfmFunction setting * Update CHANGELOG.md issue 番号を追加 * Update index.d.ts * change 'functionPicker' to 'mfmFunctionPicker' * Change indent from 4 space to 1 tab --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
49e2eb87e9
commit
b0301dd2fb
@ -19,8 +19,9 @@
|
||||
- Fix: 自分のdirect noteがuser list timelineに追加されない
|
||||
|
||||
### Client
|
||||
- Fix: 一部のモデログ(logYellowでの表示対象)について、表示の色が変わらない問題を修正
|
||||
- Feat: AiScript専用のMFM構文`$[clickable.ev=EVENTNAME ...]`を追加。`Mk:C:mfm`のオプション`onClickEv`に関数を渡すと、クリック時に`EVENTNAME`を引数にして呼び出す
|
||||
- Enhance: MFM入力補助ボタンを投稿フォームに表示できるように #12787
|
||||
- Fix: 一部のモデログ(logYellowでの表示対象)について、表示の色が変わらない問題を修正
|
||||
- Fix: `fg`/`bg`MFMに長い単語を指定すると、オーバーフローされずはみ出る問題を修正
|
||||
|
||||
### Server
|
||||
|
2
locales/index.d.ts
vendored
2
locales/index.d.ts
vendored
@ -1220,6 +1220,8 @@ export interface Locale {
|
||||
"overwriteContentConfirm": string;
|
||||
"seasonalScreenEffect": string;
|
||||
"decorate": string;
|
||||
"addMfmFunction": string;
|
||||
"enableQuickAddMfmFunction": string;
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
|
@ -1217,6 +1217,8 @@ remainingN: "残り: {n}"
|
||||
overwriteContentConfirm: "現在の内容に上書きされますがよろしいですか?"
|
||||
seasonalScreenEffect: "季節に応じた画面の演出"
|
||||
decorate: "デコる"
|
||||
addMfmFunction: "装飾を追加"
|
||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
@ -86,6 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ph-hash ph-bold ph-lg"></i></button>
|
||||
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ph-plug ph-bold ph-lg"></i></button>
|
||||
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ph-smiley ph-bold ph-lg"></i></button>
|
||||
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ph-palette ph-bold ph-lg"></i></button>
|
||||
</div>
|
||||
<div :class="$style.footerRight">
|
||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ph-eye ph-bold ph-lg"></i></button>
|
||||
@ -127,6 +128,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
@ -184,6 +186,8 @@ const poll = ref<{
|
||||
const useCw = ref<boolean>(!!props.initialCw);
|
||||
const showPreview = ref(defaultStore.state.showPreview);
|
||||
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
|
||||
const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction);
|
||||
watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value));
|
||||
const cw = ref<string | null>(props.initialCw ?? null);
|
||||
const localOnly = ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
|
||||
const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]);
|
||||
@ -870,6 +874,14 @@ async function insertEmoji(ev: MouseEvent) {
|
||||
);
|
||||
}
|
||||
|
||||
async function insertMfmFunction(ev: MouseEvent) {
|
||||
mfmFunctionPicker(
|
||||
ev.currentTarget ?? ev.target,
|
||||
textareaEl.value,
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
function showActions(ev) {
|
||||
os.popupMenu(postFormActions.map(action => ({
|
||||
text: action.title,
|
||||
|
@ -52,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="expandLongNote">Always expand long notes</MkSwitch>
|
||||
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
|
||||
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
|
||||
<MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch>
|
||||
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
|
||||
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
|
||||
<MkSwitch v-model="showTickerOnReplies">Show instance ticker on replies</MkSwitch>
|
||||
@ -296,6 +297,7 @@ const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
|
||||
const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
|
||||
const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
|
||||
const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
|
||||
const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
|
||||
const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
|
||||
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
|
||||
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
|
||||
|
61
packages/frontend/src/scripts/mfm-function-picker.ts
Normal file
61
packages/frontend/src/scripts/mfm-function-picker.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Ref, nextTick } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { MFM_TAGS } from '@/const.js';
|
||||
|
||||
/**
|
||||
* MFMの装飾のリストを表示する
|
||||
*/
|
||||
export function mfmFunctionPicker(src: any, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
|
||||
return new Promise((res, rej) => {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.addMfmFunction,
|
||||
type: 'label',
|
||||
}, ...getFunctionList(textArea, textRef)], src);
|
||||
});
|
||||
}
|
||||
|
||||
function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) : object[] {
|
||||
const ret: object[] = [];
|
||||
MFM_TAGS.forEach(tag => {
|
||||
ret.push({
|
||||
text: tag,
|
||||
icon: 'ti ti-icons',
|
||||
action: () => add(textArea, textRef, tag),
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, type: string) {
|
||||
const caretStart: number = textArea.selectionStart as number;
|
||||
const caretEnd: number = textArea.selectionEnd as number;
|
||||
|
||||
MFM_TAGS.forEach(tag => {
|
||||
if (type === tag) {
|
||||
if (caretStart === caretEnd) {
|
||||
// 単純にFunctionを追加
|
||||
const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ]${textRef.value.substring(caretEnd)}`;
|
||||
textRef.value = trimmedText;
|
||||
} else {
|
||||
// 選択範囲を囲むようにFunctionを追加
|
||||
const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ${textRef.value.substring(caretStart, caretEnd)}]${textRef.value.substring(caretEnd)}`;
|
||||
textRef.value = trimmedText;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const nextCaretStart: number = caretStart + 3 + type.length;
|
||||
const nextCaretEnd: number = caretEnd + 3 + type.length;
|
||||
|
||||
// キャレットを戻す
|
||||
nextTick(() => {
|
||||
textArea.focus();
|
||||
textArea.setSelectionRange(nextCaretStart, nextCaretEnd);
|
||||
});
|
||||
}
|
@ -239,6 +239,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
enableQuickAddMfmFunction: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
loadRawImages: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
|
Loading…
Reference in New Issue
Block a user