feat(client): 投稿フォームのボタンの説明を表示するように (#6408)
* Add title attr with buttons on the post form * fix * tooltip * missing ; * remove title attr * fix bug * Update reactions-viewer.details.vue * help wip * ok! * i18n Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
f2964101d1
commit
111eb43fd9
13 changed files with 207 additions and 105 deletions
|
@ -45,6 +45,7 @@ loadMore: "もっと見る"
|
|||
youGotNewFollower: "フォローされました"
|
||||
receiveFollowRequest: "フォローリクエストされました"
|
||||
followRequestAccepted: "フォローが承認されました"
|
||||
mention: "メンション"
|
||||
mentions: "あなた宛て"
|
||||
directNotes: "ダイレクト投稿"
|
||||
importAndExport: "インポートとエクスポート"
|
||||
|
@ -104,6 +105,7 @@ suspendConfirm: "凍結しますか?"
|
|||
unsuspendConfirm: "解凍しますか?"
|
||||
selectList: "リストを選択"
|
||||
customEmojis: "カスタム絵文字"
|
||||
emoji: "絵文字"
|
||||
emojiName: "絵文字名"
|
||||
emojiUrl: "絵文字画像URL"
|
||||
addEmoji: "絵文字を追加"
|
||||
|
@ -510,6 +512,9 @@ serviceworkerInfo: "プッシュ通知を行うには有効する必要があり
|
|||
deletedNote: "削除された投稿"
|
||||
invisibleNote: "非公開の投稿"
|
||||
enableInfiniteScroll: "自動でもっと見る"
|
||||
visibility: "公開範囲"
|
||||
poll: "アンケート"
|
||||
useCw: "内容を隠す"
|
||||
|
||||
_theme:
|
||||
explore: "テーマを探す"
|
||||
|
@ -648,7 +653,6 @@ _cw:
|
|||
show: "もっと見る"
|
||||
chars: "{count}文字"
|
||||
files: "{count}ファイル"
|
||||
poll: "アンケート"
|
||||
|
||||
_poll:
|
||||
noOnlyOneChoice: "選択肢は最低2つ必要です"
|
||||
|
@ -1119,3 +1123,4 @@ _notification:
|
|||
youReceivedFollowRequest: "フォローリクエストが来ました"
|
||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||
youWereInvitedToGroup: "グループに招待されました"
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ export default Vue.extend({
|
|||
return concat([
|
||||
this.note.text ? [this.$t('_cw.chars', { count: length(this.note.text) })] : [],
|
||||
this.note.files && this.note.files.length !== 0 ? [this.$t('_cw.files', { count: this.note.files.length }) ] : [],
|
||||
this.note.poll != null ? [this.$t('_cw.poll')] : []
|
||||
this.note.poll != null ? [this.$t('poll')] : []
|
||||
] as string[][]).join(' / ');
|
||||
}
|
||||
},
|
||||
|
|
|
@ -62,13 +62,13 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
onMouseover() {
|
||||
if (isDeviceTouch()) return;
|
||||
if (isDeviceTouch) return;
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.showTimer = setTimeout(this.showPreview, 500);
|
||||
},
|
||||
onMouseleave() {
|
||||
if (isDeviceTouch()) return;
|
||||
if (isDeviceTouch) return;
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.hideTimer = setTimeout(this.closePreview, 500);
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
<button v-if="!fixed" class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button>
|
||||
<div>
|
||||
<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span>
|
||||
<button class="_button visibility" @click="setVisibility" ref="visibilityButton">
|
||||
<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')">
|
||||
<span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span>
|
||||
<span v-if="visibility === 'home'"><fa :icon="faHome"/></span>
|
||||
<span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span>
|
||||
<span v-if="visibility === 'specified'"><fa :icon="faEnvelope"/></span>
|
||||
</button>
|
||||
<button class="_button localOnly" v-if="visibility !== 'specified'" @click="localOnly = !localOnly" :class="{ active: localOnly }"><fa :icon="faBiohazard"/></button>
|
||||
<button class="_button localOnly" v-if="visibility !== 'specified'" @click="localOnly = !localOnly" :class="{ active: localOnly }" v-tooltip="$t('_visibility.localOnly')"><fa :icon="faBiohazard"/></button>
|
||||
<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -26,7 +26,7 @@
|
|||
<div v-if="visibility === 'specified'" class="to-specified">
|
||||
<span style="margin-right: 8px;">{{ $t('recipient') }}</span>
|
||||
<div class="visibleUsers">
|
||||
<span v-for="u in visibleUsers">
|
||||
<span v-for="u in visibleUsers" :key="u.id">
|
||||
<mk-acct :user="u"/>
|
||||
<button class="_button" @click="removeVisibleUser(u)"><fa :icon="faTimes"/></button>
|
||||
</span>
|
||||
|
@ -39,11 +39,11 @@
|
|||
<x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
|
||||
<x-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
|
||||
<footer>
|
||||
<button class="_button" @click="chooseFileFrom"><fa :icon="faPhotoVideo"/></button>
|
||||
<button class="_button" @click="poll = !poll" :class="{ active: poll }"><fa :icon="faPollH"/></button>
|
||||
<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }"><fa :icon="faEyeSlash"/></button>
|
||||
<button class="_button" @click="insertMention"><fa :icon="faAt"/></button>
|
||||
<button class="_button" @click="insertEmoji"><fa :icon="faLaughSquint"/></button>
|
||||
<button class="_button" @click="chooseFileFrom" v-tooltip="$t('attachFile')"><fa :icon="faPhotoVideo"/></button>
|
||||
<button class="_button" @click="poll = !poll" :class="{ active: poll }" v-tooltip="$t('poll')"><fa :icon="faPollH"/></button>
|
||||
<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$t('useCw')"><fa :icon="faEyeSlash"/></button>
|
||||
<button class="_button" @click="insertMention" v-tooltip="$t('mention')"><fa :icon="faAt"/></button>
|
||||
<button class="_button" @click="insertEmoji" v-tooltip="$t('emoji')"><fa :icon="faLaughSquint"/></button>
|
||||
</footer>
|
||||
<input ref="file" class="file _button" type="file" multiple="multiple" @change="onChangeFile"/>
|
||||
</div>
|
||||
|
@ -576,7 +576,7 @@ export default Vue.extend({
|
|||
insertTextAtCursor(this.$refs.text, emoji);
|
||||
vm.close();
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
<template>
|
||||
<transition name="zoom-in-top">
|
||||
<div class="buebdbiu" ref="popover" v-if="show">
|
||||
<template v-if="users.length <= 10">
|
||||
<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
|
||||
<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
|
||||
<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
|
||||
</b>
|
||||
</template>
|
||||
<template v-if="10 < users.length">
|
||||
<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
|
||||
<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
|
||||
<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
|
||||
</b>
|
||||
<span slot="omitted">+{{ count - 10 }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
<mk-tooltip :source="source" ref="tooltip">
|
||||
<template v-if="users.length <= 10">
|
||||
<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
|
||||
<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
|
||||
<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
|
||||
</b>
|
||||
</template>
|
||||
<template v-if="10 < users.length">
|
||||
<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
|
||||
<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
|
||||
<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
|
||||
</b>
|
||||
<span slot="omitted">+{{ count - 10 }}</span>
|
||||
</template>
|
||||
</mk-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import MkTooltip from './ui/tooltip.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
MkTooltip
|
||||
},
|
||||
props: {
|
||||
reaction: {
|
||||
type: String,
|
||||
|
@ -39,77 +41,11 @@ export default Vue.extend({
|
|||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.show = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
const popover = this.$refs.popover as any;
|
||||
|
||||
if (this.source == null) {
|
||||
this.destroyDom();
|
||||
return;
|
||||
}
|
||||
const rect = this.source.getBoundingClientRect();
|
||||
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + this.source.offsetHeight;
|
||||
popover.style.left = (x - 28) + 'px';
|
||||
popover.style.top = (y + 16) + 'px';
|
||||
});
|
||||
}
|
||||
methods: {
|
||||
close() {
|
||||
this.show = false;
|
||||
setTimeout(this.destroyDom, 300);
|
||||
this.$refs.tooltip.close();
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.buebdbiu {
|
||||
z-index: 10000;
|
||||
display: block;
|
||||
position: absolute;
|
||||
max-width: 240px;
|
||||
font-size: 0.8em;
|
||||
padding: 6px 8px;
|
||||
background: var(--panel);
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
||||
pointer-events: none;
|
||||
transform-origin: center -16px;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
left: 12px;
|
||||
border-top: solid 14px transparent;
|
||||
border-right: solid 14px transparent;
|
||||
border-bottom: solid 14px rgba(0,0,0,0.1);
|
||||
border-left: solid 14px transparent;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -27px;
|
||||
left: 12px;
|
||||
border-top: solid 14px transparent;
|
||||
border-right: solid 14px transparent;
|
||||
border-bottom: solid 14px var(--panel);
|
||||
border-left: solid 14px transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||
@click="toggleReaction(reaction)"
|
||||
v-if="count > 0"
|
||||
@touchstart="onMouseover"
|
||||
@mouseover="onMouseover"
|
||||
@mouseleave="onMouseleave"
|
||||
@touchend="onMouseleave"
|
||||
ref="reaction"
|
||||
v-particle
|
||||
>
|
||||
|
@ -90,16 +92,17 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
onMouseover() {
|
||||
if (this.isHovering) return;
|
||||
this.isHovering = true;
|
||||
this.detailsTimeoutId = setTimeout(this.openDetails, 300);
|
||||
},
|
||||
onMouseleave() {
|
||||
if (!this.isHovering) return;
|
||||
this.isHovering = false;
|
||||
clearTimeout(this.detailsTimeoutId);
|
||||
this.closeDetails();
|
||||
},
|
||||
openDetails() {
|
||||
if (this.$root.isMobile) return;
|
||||
this.$root.api('notes/reactions', {
|
||||
noteId: this.note.id,
|
||||
type: this.reaction,
|
||||
|
|
96
src/client/components/ui/tooltip.vue
Normal file
96
src/client/components/ui/tooltip.vue
Normal file
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<transition name="zoom-in-top" appear>
|
||||
<div class="buebdbiu" v-if="show">
|
||||
<slot>{{ text }}</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
source: {
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
show: false
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.show = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.source == null) {
|
||||
this.destroyDom();
|
||||
return;
|
||||
}
|
||||
const rect = this.source.getBoundingClientRect();
|
||||
|
||||
const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
|
||||
const y = rect.top + window.pageYOffset + this.source.offsetHeight;
|
||||
this.$el.style.left = (x - 28) + 'px';
|
||||
this.$el.style.top = (y + 16) + 'px';
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.show = false;
|
||||
setTimeout(this.destroyDom, 300);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.buebdbiu {
|
||||
z-index: 11000;
|
||||
display: block;
|
||||
position: absolute;
|
||||
max-width: 240px;
|
||||
font-size: 0.8em;
|
||||
padding: 6px 8px;
|
||||
background: var(--panel);
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
||||
pointer-events: none;
|
||||
transform-origin: center -16px;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
left: 12px;
|
||||
border-top: solid 14px transparent;
|
||||
border-right: solid 14px transparent;
|
||||
border-bottom: solid 14px rgba(0,0,0,0.1);
|
||||
border-left: solid 14px transparent;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -27px;
|
||||
left: 12px;
|
||||
border-top: solid 14px transparent;
|
||||
border-right: solid 14px transparent;
|
||||
border-bottom: solid 14px var(--panel);
|
||||
border-left: solid 14px transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -93,13 +93,13 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
onMouseover() {
|
||||
if (isDeviceTouch()) return;
|
||||
if (isDeviceTouch) return;
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.showTimer = setTimeout(this.showPreview, 500);
|
||||
},
|
||||
onMouseleave() {
|
||||
if (isDeviceTouch()) return;
|
||||
if (isDeviceTouch) return;
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.hideTimer = setTimeout(this.closePreview, 500);
|
||||
|
|
|
@ -4,9 +4,11 @@ import userPreview from './user-preview';
|
|||
import autocomplete from './autocomplete';
|
||||
import size from './size';
|
||||
import particle from './particle';
|
||||
import tooltip from './tooltip';
|
||||
|
||||
Vue.directive('autocomplete', autocomplete);
|
||||
Vue.directive('userPreview', userPreview);
|
||||
Vue.directive('user-preview', userPreview);
|
||||
Vue.directive('size', size);
|
||||
Vue.directive('particle', particle);
|
||||
Vue.directive('tooltip', tooltip);
|
||||
|
|
62
src/client/directives/tooltip.ts
Normal file
62
src/client/directives/tooltip.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import MkTooltip from '../components/ui/tooltip.vue';
|
||||
import { isDeviceTouch } from '../scripts/is-device-touch';
|
||||
|
||||
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
|
||||
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
|
||||
|
||||
export default {
|
||||
bind(el: HTMLElement, binding, vn) {
|
||||
const self = (el as any)._tooltipDirective_ = {} as any;
|
||||
|
||||
self.text = binding.value as string;
|
||||
self.tag = null;
|
||||
self.showTimer = null;
|
||||
self.hideTimer = null;
|
||||
self.checkTimer = null;
|
||||
|
||||
self.close = () => {
|
||||
if (self.tag) {
|
||||
clearInterval(self.checkTimer);
|
||||
self.tag.close();
|
||||
self.tag = null;
|
||||
}
|
||||
};
|
||||
|
||||
const show = e => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (self.tag) return;
|
||||
|
||||
self.tag = new MkTooltip({
|
||||
parent: vn.context,
|
||||
propsData: {
|
||||
text: self.text,
|
||||
source: el
|
||||
}
|
||||
}).$mount();
|
||||
|
||||
document.body.appendChild(self.tag.$el);
|
||||
};
|
||||
|
||||
el.addEventListener(start, () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.showTimer = setTimeout(show, 300);
|
||||
});
|
||||
|
||||
el.addEventListener(end, () => {
|
||||
clearTimeout(self.showTimer);
|
||||
clearTimeout(self.hideTimer);
|
||||
self.hideTimer = setTimeout(self.close, 300);
|
||||
});
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
clearTimeout(self.showTimer);
|
||||
self.close();
|
||||
});
|
||||
},
|
||||
|
||||
unbind(el, binding, vn) {
|
||||
const self = el._tooltipDirective_;
|
||||
clearInterval(self.checkTimer);
|
||||
},
|
||||
};
|
|
@ -5,7 +5,7 @@ import { clientDb, get, bulkGet } from '../db';
|
|||
const getTranslation = (text: string): Promise<string> => get(text, clientDb.i18n);
|
||||
|
||||
export default async function(type, data): Promise<[string, NotificationOptions]> {
|
||||
const contexts = ['deletedNote', 'invisibleNote', 'withNFiles', '_cw.poll'];
|
||||
const contexts = ['deletedNote', 'invisibleNote', 'withNFiles', 'poll'];
|
||||
const locale = Object.fromEntries(await bulkGet(contexts, clientDb.i18n) as [string, string][]);
|
||||
|
||||
switch (type) {
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
export function isDeviceTouch(): boolean {
|
||||
return 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
|
||||
}
|
||||
export const isDeviceTouch = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
|
||||
|
|
|
@ -27,7 +27,7 @@ const summarize = (note: any, locale: any): string => {
|
|||
|
||||
// 投票が添付されているとき
|
||||
if (note.poll) {
|
||||
summary += ` (${locale._cw?.poll || locale['_cw.poll']})`;
|
||||
summary += ` (${locale['poll']})`;
|
||||
}
|
||||
|
||||
// 返信のとき
|
||||
|
|
Loading…
Reference in a new issue