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
|
### General
|
||||||
-
|
-
|
||||||
|
@ -38,6 +38,7 @@
|
||||||
- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように
|
- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように
|
||||||
- Enhance: Playの説明欄にMFMを使えるように
|
- Enhance: Playの説明欄にMFMを使えるように
|
||||||
- Enhance: チャンネルノートの場合は詳細ページからその前後のノートを見れるように
|
- Enhance: チャンネルノートの場合は詳細ページからその前後のノートを見れるように
|
||||||
|
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
||||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||||
|
|
|
@ -35,6 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span>{{ tag }}</span>
|
<span>{{ tag }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -51,7 +56,7 @@ import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { customEmojis } from '@/custom-emojis.js';
|
import { customEmojis } from '@/custom-emojis.js';
|
||||||
import { MFM_TAGS } from '@/const.js';
|
import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
|
||||||
|
|
||||||
type EmojiDef = {
|
type EmojiDef = {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
|
@ -130,7 +135,7 @@ export default {
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: string;
|
type: string;
|
||||||
q: string | null;
|
q: any;
|
||||||
textarea: HTMLTextAreaElement;
|
textarea: HTMLTextAreaElement;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
x: number;
|
x: number;
|
||||||
|
@ -151,6 +156,7 @@ const hashtags = ref<any[]>([]);
|
||||||
const emojis = ref<(EmojiDef)[]>([]);
|
const emojis = ref<(EmojiDef)[]>([]);
|
||||||
const items = ref<Element[] | HTMLCollection>([]);
|
const items = ref<Element[] | HTMLCollection>([]);
|
||||||
const mfmTags = ref<string[]>([]);
|
const mfmTags = ref<string[]>([]);
|
||||||
|
const mfmParams = ref<string[]>([]);
|
||||||
const select = ref(-1);
|
const select = ref(-1);
|
||||||
const zIndex = os.claimZIndex('high');
|
const zIndex = os.claimZIndex('high');
|
||||||
|
|
||||||
|
@ -251,6 +257,13 @@ function exec() {
|
||||||
}
|
}
|
||||||
|
|
||||||
mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q ?? ''));
|
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 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_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 { toASCII } from 'punycode/';
|
||||||
import { popup } from '@/os.js';
|
import { popup } from '@/os.js';
|
||||||
|
|
||||||
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag';
|
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
|
||||||
|
|
||||||
export class Autocomplete {
|
export class Autocomplete {
|
||||||
private suggestion: {
|
private suggestion: {
|
||||||
x: Ref<number>;
|
x: Ref<number>;
|
||||||
y: Ref<number>;
|
y: Ref<number>;
|
||||||
q: Ref<string | null>;
|
q: Ref<any>;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
} | null;
|
} | null;
|
||||||
private textarea: HTMLInputElement | HTMLTextAreaElement;
|
private textarea: HTMLInputElement | HTMLTextAreaElement;
|
||||||
|
@ -49,7 +49,7 @@ export class Autocomplete {
|
||||||
this.textarea = textarea;
|
this.textarea = textarea;
|
||||||
this.textRef = textRef;
|
this.textRef = textRef;
|
||||||
this.opening = false;
|
this.opening = false;
|
||||||
this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag'];
|
this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam'];
|
||||||
|
|
||||||
this.attach();
|
this.attach();
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,7 @@ export class Autocomplete {
|
||||||
const hashtagIndex = text.lastIndexOf('#');
|
const hashtagIndex = text.lastIndexOf('#');
|
||||||
const emojiIndex = text.lastIndexOf(':');
|
const emojiIndex = text.lastIndexOf(':');
|
||||||
const mfmTagIndex = text.lastIndexOf('$');
|
const mfmTagIndex = text.lastIndexOf('$');
|
||||||
|
const mfmParamIndex = text.lastIndexOf('.');
|
||||||
|
|
||||||
const max = Math.max(
|
const max = Math.max(
|
||||||
mentionIndex,
|
mentionIndex,
|
||||||
|
@ -94,7 +95,8 @@ export class Autocomplete {
|
||||||
|
|
||||||
const isMention = mentionIndex !== -1;
|
const isMention = mentionIndex !== -1;
|
||||||
const isHashtag = hashtagIndex !== -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(':');
|
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
|
||||||
|
|
||||||
let opened = false;
|
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) {
|
if (!opened) {
|
||||||
this.close();
|
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) {
|
if (type !== this.currentType) {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
@ -280,6 +293,22 @@ export class Autocomplete {
|
||||||
const pos = trimmedBefore.length + (value.length + 3);
|
const pos = trimmedBefore.length + (value.length + 3);
|
||||||
this.textarea.setSelectionRange(pos, pos);
|
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