Enhance(frontend): MFMの属性にオートコンプリートが利用できるように (#12803)
* MFMのパラメータでオートコンプリートできるように * tweak conditions & refactor * ファイル末尾の改行忘れ * remove console.log & refactor * 型付けに敗北 * fix * update CHANGELOG.md * tweak conditions * CHANGELOGの様式ミス * CHANGELOGを書く場所を間違えていたので修正 * move changelog * move changelog * typeof MFM_TAGS[number] Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * $[border.noclip ]対応 * Update const.ts --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
b17eb8e537
commit
678dba9245
4 changed files with 75 additions and 8 deletions
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
## 2023.x.x (unreleased)
|
||||
## 202x.x.x (unreleased)
|
||||
|
||||
### General
|
||||
-
|
||||
|
@ -38,6 +38,7 @@
|
|||
- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように
|
||||
- Enhance: Playの説明欄にMFMを使えるように
|
||||
- Enhance: チャンネルノートの場合は詳細ページからその前後のノートを見れるように
|
||||
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||
|
|
|
@ -35,6 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span>{{ tag }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<ol v-else-if="mfmParams.length > 0" ref="suggests" :class="$style.list">
|
||||
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="complete(type, q.params.toSpliced(-1, 1, param).join(','))" @keydown="onKeydown">
|
||||
<span>{{ param }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -51,7 +56,7 @@ import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { MFM_TAGS } from '@/const.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
|
||||
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
|
@ -130,7 +135,7 @@ export default {
|
|||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
q: string | null;
|
||||
q: any;
|
||||
textarea: HTMLTextAreaElement;
|
||||
close: () => void;
|
||||
x: number;
|
||||
|
@ -151,6 +156,7 @@ const hashtags = ref<any[]>([]);
|
|||
const emojis = ref<(EmojiDef)[]>([]);
|
||||
const items = ref<Element[] | HTMLCollection>([]);
|
||||
const mfmTags = ref<string[]>([]);
|
||||
const mfmParams = ref<string[]>([]);
|
||||
const select = ref(-1);
|
||||
const zIndex = os.claimZIndex('high');
|
||||
|
||||
|
@ -251,6 +257,13 @@ function exec() {
|
|||
}
|
||||
|
||||
mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q ?? ''));
|
||||
} else if (props.type === 'mfmParam') {
|
||||
if (props.q.params.at(-1) === '') {
|
||||
mfmParams.value = MFM_PARAMS[props.q.tag] ?? [];
|
||||
return;
|
||||
}
|
||||
|
||||
mfmParams.value = MFM_PARAMS[props.q.tag].filter(param => param.startsWith(props.q.params.at(-1) ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -109,3 +109,27 @@ export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-foun
|
|||
export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg';
|
||||
|
||||
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
|
||||
export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
|
||||
tada: ['speed=', 'delay='],
|
||||
jelly: ['speed=', 'delay='],
|
||||
twitch: ['speed=', 'delay='],
|
||||
shake: ['speed=', 'delay='],
|
||||
spin: ['speed=', 'delay=', 'left', 'alternate', 'x', 'y'],
|
||||
jump: ['speed=', 'delay='],
|
||||
bounce: ['speed=', 'delay='],
|
||||
flip: ['h', 'v'],
|
||||
x2: [],
|
||||
x3: [],
|
||||
x4: [],
|
||||
scale: ['x=', 'y='],
|
||||
position: ['x=', 'y='],
|
||||
fg: ['color='],
|
||||
bg: ['color='],
|
||||
border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
|
||||
font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'],
|
||||
blur: [],
|
||||
rainbow: ['speed=', 'delay='],
|
||||
rotate: ['deg='],
|
||||
ruby: [],
|
||||
unixtime: [],
|
||||
};
|
||||
|
|
|
@ -8,13 +8,13 @@ import getCaretCoordinates from 'textarea-caret';
|
|||
import { toASCII } from 'punycode/';
|
||||
import { popup } from '@/os.js';
|
||||
|
||||
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag';
|
||||
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
|
||||
|
||||
export class Autocomplete {
|
||||
private suggestion: {
|
||||
x: Ref<number>;
|
||||
y: Ref<number>;
|
||||
q: Ref<string | null>;
|
||||
q: Ref<any>;
|
||||
close: () => void;
|
||||
} | null;
|
||||
private textarea: HTMLInputElement | HTMLTextAreaElement;
|
||||
|
@ -49,7 +49,7 @@ export class Autocomplete {
|
|||
this.textarea = textarea;
|
||||
this.textRef = textRef;
|
||||
this.opening = false;
|
||||
this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag'];
|
||||
this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam'];
|
||||
|
||||
this.attach();
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ export class Autocomplete {
|
|||
const hashtagIndex = text.lastIndexOf('#');
|
||||
const emojiIndex = text.lastIndexOf(':');
|
||||
const mfmTagIndex = text.lastIndexOf('$');
|
||||
const mfmParamIndex = text.lastIndexOf('.');
|
||||
|
||||
const max = Math.max(
|
||||
mentionIndex,
|
||||
|
@ -94,7 +95,8 @@ export class Autocomplete {
|
|||
|
||||
const isMention = mentionIndex !== -1;
|
||||
const isHashtag = hashtagIndex !== -1;
|
||||
const isMfmTag = mfmTagIndex !== -1;
|
||||
const isMfmParam = mfmParamIndex !== -1 && text.split(/\$\[[a-zA-Z]+/).pop()?.includes('.');
|
||||
const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
|
||||
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
|
||||
|
||||
let opened = false;
|
||||
|
@ -134,6 +136,17 @@ export class Autocomplete {
|
|||
}
|
||||
}
|
||||
|
||||
if (isMfmParam && !opened && this.onlyType.includes('mfmParam')) {
|
||||
const mfmParam = text.substring(mfmParamIndex + 1);
|
||||
if (!mfmParam.includes(' ')) {
|
||||
this.open('mfmParam', {
|
||||
tag: text.substring(mfmTagIndex + 2, mfmParamIndex),
|
||||
params: mfmParam.split(','),
|
||||
});
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!opened) {
|
||||
this.close();
|
||||
}
|
||||
|
@ -142,7 +155,7 @@ export class Autocomplete {
|
|||
/**
|
||||
* サジェストを提示します。
|
||||
*/
|
||||
private async open(type: string, q: string | null) {
|
||||
private async open(type: string, q: any) {
|
||||
if (type !== this.currentType) {
|
||||
this.close();
|
||||
}
|
||||
|
@ -280,6 +293,22 @@ export class Autocomplete {
|
|||
const pos = trimmedBefore.length + (value.length + 3);
|
||||
this.textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
} else if (type === 'mfmParam') {
|
||||
const source = this.text;
|
||||
|
||||
const before = source.substring(0, caret);
|
||||
const trimmedBefore = before.substring(0, before.lastIndexOf('.'));
|
||||
const after = source.substring(caret);
|
||||
|
||||
// 挿入
|
||||
this.text = `${trimmedBefore}.${value}${after}`;
|
||||
|
||||
// キャレットを戻す
|
||||
nextTick(() => {
|
||||
this.textarea.focus();
|
||||
const pos = trimmedBefore.length + (value.length + 1);
|
||||
this.textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue