feat(client): MFM関数構文のサジェストを実装
This commit is contained in:
parent
a75f3fb87c
commit
a70dbb7e74
3 changed files with 58 additions and 19 deletions
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
- クライアント: アニメーションを減らす設定をメニューのアニメーションにも適用するように
|
- クライアント: アニメーションを減らす設定をメニューのアニメーションにも適用するように
|
||||||
|
- クライアント: MFM関数構文のサジェストを実装
|
||||||
- ActivityPub: HTML -> MFMの変換を強化
|
- ActivityPub: HTML -> MFMの変換を強化
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
|
|
|
@ -10,12 +10,12 @@
|
||||||
</li>
|
</li>
|
||||||
<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $ts.selectUser }}</li>
|
<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $ts.selectUser }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
<ol class="hashtags" ref="suggests" v-if="hashtags.length > 0">
|
<ol class="hashtags" ref="suggests" v-else-if="hashtags.length > 0">
|
||||||
<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
|
<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
|
||||||
<span class="name">{{ hashtag }}</span>
|
<span class="name">{{ hashtag }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
|
<ol class="emojis" ref="suggests" v-else-if="emojis.length > 0">
|
||||||
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
|
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
|
||||||
<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
|
<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
|
||||||
<span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
|
<span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
|
||||||
|
@ -24,6 +24,11 @@
|
||||||
<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
|
<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
<ol class="mfmTags" ref="suggests" v-else-if="mfmTags.length > 0">
|
||||||
|
<li v-for="tag in mfmTags" @click="complete(type, tag)" @keydown="onKeydown" tabindex="-1">
|
||||||
|
<span class="tag">{{ tag }}</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -106,6 +111,8 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
|
||||||
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
|
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle'];
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
type: {
|
type: {
|
||||||
|
@ -137,11 +144,6 @@ export default defineComponent({
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
showing: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ['done', 'closed'],
|
emits: ['done', 'closed'],
|
||||||
|
@ -154,18 +156,11 @@ export default defineComponent({
|
||||||
hashtags: [],
|
hashtags: [],
|
||||||
emojis: [],
|
emojis: [],
|
||||||
items: [],
|
items: [],
|
||||||
|
mfmTags: [],
|
||||||
select: -1,
|
select: -1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
showing() {
|
|
||||||
if (!this.showing) {
|
|
||||||
this.$emit('closed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
updated() {
|
||||||
this.setPosition();
|
this.setPosition();
|
||||||
this.items = (this.$refs.suggests as Element | undefined)?.children || [];
|
this.items = (this.$refs.suggests as Element | undefined)?.children || [];
|
||||||
|
@ -236,7 +231,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.type == 'user') {
|
console.log(this.type);
|
||||||
|
|
||||||
|
if (this.type === 'user') {
|
||||||
if (this.q == null) {
|
if (this.q == null) {
|
||||||
this.users = [];
|
this.users = [];
|
||||||
this.fetching = false;
|
this.fetching = false;
|
||||||
|
@ -262,7 +259,7 @@ export default defineComponent({
|
||||||
sessionStorage.setItem(cacheKey, JSON.stringify(users));
|
sessionStorage.setItem(cacheKey, JSON.stringify(users));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (this.type == 'hashtag') {
|
} else if (this.type === 'hashtag') {
|
||||||
if (this.q == null || this.q == '') {
|
if (this.q == null || this.q == '') {
|
||||||
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
|
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
|
||||||
this.fetching = false;
|
this.fetching = false;
|
||||||
|
@ -286,7 +283,7 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.type == 'emoji') {
|
} else if (this.type === 'emoji') {
|
||||||
if (this.q == null || this.q == '') {
|
if (this.q == null || this.q == '') {
|
||||||
// 最近使った絵文字をサジェスト
|
// 最近使った絵文字をサジェスト
|
||||||
this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
|
this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
|
||||||
|
@ -314,6 +311,14 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emojis = matched;
|
this.emojis = matched;
|
||||||
|
} else if (this.type === 'mfmTag') {
|
||||||
|
console.log(this.q);
|
||||||
|
if (this.q == null || this.q == '') {
|
||||||
|
this.mfmTags = MFM_TAGS;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -490,5 +495,11 @@ export default defineComponent({
|
||||||
margin: 0 0 0 8px;
|
margin: 0 0 0 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .mfmTags > li {
|
||||||
|
|
||||||
|
.name {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -70,11 +70,13 @@ export class Autocomplete {
|
||||||
const mentionIndex = text.lastIndexOf('@');
|
const mentionIndex = text.lastIndexOf('@');
|
||||||
const hashtagIndex = text.lastIndexOf('#');
|
const hashtagIndex = text.lastIndexOf('#');
|
||||||
const emojiIndex = text.lastIndexOf(':');
|
const emojiIndex = text.lastIndexOf(':');
|
||||||
|
const mfmTagIndex = text.lastIndexOf('$');
|
||||||
|
|
||||||
const max = Math.max(
|
const max = Math.max(
|
||||||
mentionIndex,
|
mentionIndex,
|
||||||
hashtagIndex,
|
hashtagIndex,
|
||||||
emojiIndex);
|
emojiIndex,
|
||||||
|
mfmTagIndex);
|
||||||
|
|
||||||
if (max == -1) {
|
if (max == -1) {
|
||||||
this.close();
|
this.close();
|
||||||
|
@ -83,6 +85,7 @@ export class Autocomplete {
|
||||||
|
|
||||||
const isMention = mentionIndex != -1;
|
const isMention = mentionIndex != -1;
|
||||||
const isHashtag = hashtagIndex != -1;
|
const isHashtag = hashtagIndex != -1;
|
||||||
|
const isMfmTag = mfmTagIndex != -1;
|
||||||
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;
|
||||||
|
@ -114,6 +117,14 @@ export class Autocomplete {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMfmTag && !opened) {
|
||||||
|
const mfmTag = text.substr(mfmTagIndex + 1);
|
||||||
|
if (!mfmTag.includes(' ')) {
|
||||||
|
this.open('mfmTag', mfmTag);
|
||||||
|
opened = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!opened) {
|
if (!opened) {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
@ -244,6 +255,22 @@ export class Autocomplete {
|
||||||
const pos = trimmedBefore.length + value.length;
|
const pos = trimmedBefore.length + value.length;
|
||||||
this.textarea.setSelectionRange(pos, pos);
|
this.textarea.setSelectionRange(pos, pos);
|
||||||
});
|
});
|
||||||
|
} else if (type == 'mfmTag') {
|
||||||
|
const source = this.text;
|
||||||
|
|
||||||
|
const before = source.substr(0, caret);
|
||||||
|
const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
|
||||||
|
const after = source.substr(caret);
|
||||||
|
|
||||||
|
// 挿入
|
||||||
|
this.text = `${trimmedBefore}$[${value} ]${after}`;
|
||||||
|
|
||||||
|
// キャレットを戻す
|
||||||
|
this.vm.$nextTick(() => {
|
||||||
|
this.textarea.focus();
|
||||||
|
const pos = trimmedBefore.length + (value.length + 3);
|
||||||
|
this.textarea.setSelectionRange(pos, pos);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue