summaryrefslogtreecommitdiff
path: root/src/client/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-08-08 23:25:21 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-08-08 23:25:21 +0900
commitc52e30e8e0fb0e84a30f5d422585de492bab59ca (patch)
treee23c4fd3ff4f089e0259f5e4e751b5bef6c18e1f /src/client/components
parentMerge branch 'develop' (diff)
parent12.85.0 (diff)
downloadmisskey-c52e30e8e0fb0e84a30f5d422585de492bab59ca.tar.gz
misskey-c52e30e8e0fb0e84a30f5d422585de492bab59ca.tar.bz2
misskey-c52e30e8e0fb0e84a30f5d422585de492bab59ca.zip
Merge branch 'develop'
Diffstat (limited to 'src/client/components')
-rw-r--r--src/client/components/abuse-report-window.vue6
-rw-r--r--src/client/components/dialog.vue4
-rw-r--r--src/client/components/drive.file.vue2
-rw-r--r--src/client/components/drive.vue15
-rw-r--r--src/client/components/emoji-picker-dialog.vue14
-rw-r--r--src/client/components/forgot-password.vue10
-rw-r--r--src/client/components/global/misskey-flavored-markdown.vue5
-rw-r--r--src/client/components/instance-stats.vue4
-rw-r--r--src/client/components/mfm.ts4
-rw-r--r--src/client/components/modal-page-window.vue25
-rw-r--r--src/client/components/note-detailed.vue8
-rw-r--r--src/client/components/note-header.vue4
-rw-r--r--src/client/components/note.vue8
-rw-r--r--src/client/components/notification-setting-window.vue6
-rw-r--r--src/client/components/notification.vue4
-rw-r--r--src/client/components/page-preview.vue2
-rw-r--r--src/client/components/page-window.vue13
-rw-r--r--src/client/components/page/page.number-input.vue4
-rw-r--r--src/client/components/page/page.post.vue2
-rw-r--r--src/client/components/page/page.switch.vue2
-rw-r--r--src/client/components/page/page.text-input.vue4
-rw-r--r--src/client/components/page/page.textarea-input.vue4
-rw-r--r--src/client/components/page/page.textarea.vue2
-rw-r--r--src/client/components/poll-editor.vue22
-rw-r--r--src/client/components/post-form-attaches.vue2
-rw-r--r--src/client/components/post-form.vue34
-rw-r--r--src/client/components/sample.vue8
-rwxr-xr-xsrc/client/components/signin.vue16
-rw-r--r--src/client/components/signup.vue44
-rw-r--r--src/client/components/token-generate-window.vue6
-rw-r--r--src/client/components/ui/button.vue6
-rw-r--r--src/client/components/ui/folder.vue3
-rw-r--r--src/client/components/ui/input.vue287
-rw-r--r--src/client/components/ui/menu.vue4
-rw-r--r--src/client/components/ui/popup-menu.vue (renamed from src/client/components/ui/modal-menu.vue)25
-rw-r--r--src/client/components/ui/popup.vue213
-rw-r--r--src/client/components/ui/select.vue319
-rw-r--r--src/client/components/ui/switch.vue10
-rw-r--r--src/client/components/ui/textarea.vue285
-rw-r--r--src/client/components/ui/window.vue10
-rw-r--r--src/client/components/user-info.vue2
-rw-r--r--src/client/components/user-list.vue2
-rw-r--r--src/client/components/user-preview.vue2
-rw-r--r--src/client/components/user-select-dialog.vue20
-rw-r--r--src/client/components/users-dialog.vue2
-rw-r--r--src/client/components/widgets.vue2
46 files changed, 822 insertions, 654 deletions
diff --git a/src/client/components/abuse-report-window.vue b/src/client/components/abuse-report-window.vue
index d9e1c3966b..266c0d566f 100644
--- a/src/client/components/abuse-report-window.vue
+++ b/src/client/components/abuse-report-window.vue
@@ -10,9 +10,9 @@
</template>
<div class="dpvffvvy _monolithic_">
<div class="_section">
- <MkTextarea v-model:value="comment">
- <span>{{ $ts.details }}</span>
- <template #desc>{{ $ts.fillAbuseReportDescription }}</template>
+ <MkTextarea v-model="comment">
+ <template #label>{{ $ts.details }}</template>
+ <template #caption>{{ $ts.fillAbuseReportDescription }}</template>
</MkTextarea>
</div>
<div class="_section">
diff --git a/src/client/components/dialog.vue b/src/client/components/dialog.vue
index a673e827d6..f3611f050e 100644
--- a/src/client/components/dialog.vue
+++ b/src/client/components/dialog.vue
@@ -14,8 +14,8 @@
</div>
<header v-if="title"><Mfm :text="title"/></header>
<div class="body" v-if="text"><Mfm :text="text"/></div>
- <MkInput v-if="input" v-model:value="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput>
- <MkSelect v-if="select" v-model:value="selectedValue" autofocus>
+ <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput>
+ <MkSelect v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
</template>
diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue
index 3d20de23e9..b1be3d0cab 100644
--- a/src/client/components/drive.file.vue
+++ b/src/client/components/drive.file.vue
@@ -114,7 +114,7 @@ export default defineComponent({
if (this.selectMode) {
this.$emit('chosen', this.file);
} else {
- os.modalMenu(this.getMenu(), ev.currentTarget || ev.target);
+ os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
}
},
diff --git a/src/client/components/drive.vue b/src/client/components/drive.vue
index 16aa9dc1a8..5dadf9a11f 100644
--- a/src/client/components/drive.vue
+++ b/src/client/components/drive.vue
@@ -10,6 +10,7 @@
<span class="separator" v-if="folder != null"><i class="fas fa-angle-right"></i></span>
<span class="folder current" v-if="folder != null">{{ folder.name }}</span>
</div>
+ <button @click="showMenu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
</nav>
<div class="main" :class="{ uploading: uploadings.length > 0, fetching }"
ref="main"
@@ -627,8 +628,12 @@ export default defineComponent({
}];
},
- onContextmenu(e) {
- os.contextMenu(this.getMenu(), e);
+ showMenu(ev) {
+ os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
+ },
+
+ onContextmenu(ev) {
+ os.contextMenu(this.getMenu(), ev);
},
}
});
@@ -641,7 +646,7 @@ export default defineComponent({
height: 100%;
> nav {
- display: block;
+ display: flex;
z-index: 2;
width: 100%;
padding: 0 8px;
@@ -696,6 +701,10 @@ export default defineComponent({
}
}
}
+
+ > .menu {
+ margin-left: auto;
+ }
}
> .main {
diff --git a/src/client/components/emoji-picker-dialog.vue b/src/client/components/emoji-picker-dialog.vue
index 9400819a1f..9aca47f547 100644
--- a/src/client/components/emoji-picker-dialog.vue
+++ b/src/client/components/emoji-picker-dialog.vue
@@ -1,17 +1,17 @@
<template>
-<MkModal ref="modal" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
- <MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen" ref="picker"/>
-</MkModal>
+<MkPopup ref="popup" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.popup.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
+ <MkEmojiPicker class="_shadow" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen" ref="picker"/>
+</MkPopup>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
-import MkModal from '@client/components/ui/modal.vue';
+import MkPopup from '@client/components/ui/popup.vue';
import MkEmojiPicker from '@client/components/emoji-picker.vue';
export default defineComponent({
components: {
- MkModal,
+ MkPopup,
MkEmojiPicker,
},
@@ -33,7 +33,7 @@ export default defineComponent({
},
},
- emits: ['done', 'closed'],
+ emits: ['done', 'close', 'closed'],
data() {
return {
@@ -44,7 +44,7 @@ export default defineComponent({
methods: {
chosen(emoji: any) {
this.$emit('done', emoji);
- this.$refs.modal.close();
+ this.$refs.popup.close();
},
opening() {
diff --git a/src/client/components/forgot-password.vue b/src/client/components/forgot-password.vue
index 1f530d7ca2..3b5ad6d6ba 100644
--- a/src/client/components/forgot-password.vue
+++ b/src/client/components/forgot-password.vue
@@ -9,14 +9,14 @@
<form class="_monolithic_" @submit.prevent="onSubmit" v-if="$instance.enableEmail">
<div class="_section">
- <MkInput v-model:value="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
- <span>{{ $ts.username }}</span>
+ <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
+ <template #label>{{ $ts.username }}</template>
<template #prefix>@</template>
</MkInput>
- <MkInput v-model:value="email" type="email" spellcheck="false" required>
- <span>{{ $ts.emailAddress }}</span>
- <template #desc>{{ $ts._forgotPassword.enterEmail }}</template>
+ <MkInput v-model="email" type="email" spellcheck="false" required>
+ <template #label>{{ $ts.emailAddress }}</template>
+ <template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
</MkInput>
<MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
diff --git a/src/client/components/global/misskey-flavored-markdown.vue b/src/client/components/global/misskey-flavored-markdown.vue
index 988cf9cf47..c4f75bee93 100644
--- a/src/client/components/global/misskey-flavored-markdown.vue
+++ b/src/client/components/global/misskey-flavored-markdown.vue
@@ -117,6 +117,11 @@ export default defineComponent({
75% { transform: scale3d(1.05, 0.95, 1); }
to { transform: scale3d(1, 1, 1); }
}
+
+@keyframes mfm-rainbow {
+ 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
+ 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
+}
</style>
<style lang="scss" scoped>
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue
index 432c9a1bb9..78044f0b16 100644
--- a/src/client/components/instance-stats.vue
+++ b/src/client/components/instance-stats.vue
@@ -1,7 +1,7 @@
<template>
<div class="zbcjwnqg" style="margin-top: -8px;">
<div class="selects" style="display: flex;">
- <MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;">
+ <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
<optgroup :label="$ts.federation">
<option value="federation-instances">{{ $ts._charts.federationInstancesIncDec }}</option>
<option value="federation-instances-total">{{ $ts._charts.federationInstancesTotal }}</option>
@@ -24,7 +24,7 @@
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
</optgroup>
</MkSelect>
- <MkSelect v-model:value="chartSpan" style="margin: 0;">
+ <MkSelect v-model="chartSpan" style="margin: 0;">
<option value="hour">{{ $ts.perHour }}</option>
<option value="day">{{ $ts.perDay }}</option>
</MkSelect>
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index 3b08c83c7f..c248f934df 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -165,6 +165,10 @@ export default defineComponent({
class: '_mfm_blur_',
}, genEl(token.children));
}
+ case 'rainbow': {
+ style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : '';
+ break;
+ }
}
if (style == null) {
return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']);
diff --git a/src/client/components/modal-page-window.vue b/src/client/components/modal-page-window.vue
index 7be4045a84..ddf8ac446e 100644
--- a/src/client/components/modal-page-window.vue
+++ b/src/client/components/modal-page-window.vue
@@ -2,12 +2,9 @@
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
<div class="hrmcaedk _popup _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div class="header" @contextmenu="onContextmenu">
- <button class="_button" @click="back()" v-if="history.length > 0"><i class="fas fa-chevron-left"></i></button>
- <button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button>
<span class="title">
- <XHeader :info="pageInfo" :with-back="false"/>
+ <XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="$refs.modal.close()"/>
</span>
- <button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
</div>
<div class="body _flat_">
<keep-alive>
@@ -177,35 +174,19 @@ export default defineComponent({
flex-shrink: 0;
box-shadow: 0px 1px var(--divider);
- > button {
- height: $height;
- width: $height;
-
- @media (max-width: 500px) {
- height: $height-narrow;
- width: $height-narrow;
- }
- }
-
> .title {
flex: 1;
- line-height: $height;
- padding-left: 32px;
+ height: $height;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- pointer-events: none;
@media (max-width: 500px) {
- line-height: $height-narrow;
+ height: $height-narrow;
padding-left: 16px;
}
}
-
- > button + .title {
- padding-left: 0;
- }
}
> .body {
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index 6040ad378f..d601052927 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -454,7 +454,7 @@ export default defineComponent({
renote(viaKeyboard = false) {
pleaseLogin();
this.blur();
- os.modalMenu([{
+ os.popupMenu([{
text: this.$ts.renote,
icon: 'fas fa-retweet',
action: () => {
@@ -743,14 +743,14 @@ export default defineComponent({
},
menu(viaKeyboard = false) {
- os.modalMenu(this.getMenu(), this.$refs.menuButton, {
+ os.popupMenu(this.getMenu(), this.$refs.menuButton, {
viaKeyboard
}).then(this.focus);
},
showRenoteMenu(viaKeyboard = false) {
if (!this.isMyRenote) return;
- os.modalMenu([{
+ os.popupMenu([{
text: this.$ts.unrenote,
icon: 'fas fa-trash-alt',
danger: true,
@@ -794,7 +794,7 @@ export default defineComponent({
async clip() {
const clips = await os.api('clips/list');
- os.modalMenu([{
+ os.popupMenu([{
icon: 'fas fa-plus',
text: this.$ts.createNew,
action: async () => {
diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue
index 1cd6463f9b..7758dea3ae 100644
--- a/src/client/components/note-header.vue
+++ b/src/client/components/note-header.vue
@@ -24,8 +24,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import notePage from '../filters/note';
-import { userPage } from '../filters/user';
+import notePage from '@client/filters/note';
+import { userPage } from '@client/filters/user';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 504d07c0eb..873b96030a 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -429,7 +429,7 @@ export default defineComponent({
renote(viaKeyboard = false) {
pleaseLogin();
this.blur();
- os.modalMenu([{
+ os.popupMenu([{
text: this.$ts.renote,
icon: 'fas fa-retweet',
action: () => {
@@ -718,14 +718,14 @@ export default defineComponent({
},
menu(viaKeyboard = false) {
- os.modalMenu(this.getMenu(), this.$refs.menuButton, {
+ os.popupMenu(this.getMenu(), this.$refs.menuButton, {
viaKeyboard
}).then(this.focus);
},
showRenoteMenu(viaKeyboard = false) {
if (!this.isMyRenote) return;
- os.modalMenu([{
+ os.popupMenu([{
text: this.$ts.unrenote,
icon: 'fas fa-trash-alt',
danger: true,
@@ -769,7 +769,7 @@ export default defineComponent({
async clip() {
const clips = await os.api('clips/list');
- os.modalMenu([{
+ os.popupMenu([{
icon: 'fas fa-plus',
text: this.$ts.createNew,
action: async () => {
diff --git a/src/client/components/notification-setting-window.vue b/src/client/components/notification-setting-window.vue
index 5f16c042bf..c33106ae15 100644
--- a/src/client/components/notification-setting-window.vue
+++ b/src/client/components/notification-setting-window.vue
@@ -11,16 +11,16 @@
<template #header>{{ $ts.notificationSetting }}</template>
<div class="_monolithic_">
<div v-if="showGlobalToggle" class="_section">
- <MkSwitch v-model:value="useGlobalSetting">
+ <MkSwitch v-model="useGlobalSetting">
{{ $ts.useGlobalSetting }}
- <template #desc>{{ $ts.useGlobalSettingDesc }}</template>
+ <template #caption>{{ $ts.useGlobalSettingDesc }}</template>
</MkSwitch>
</div>
<div v-if="!useGlobalSetting" class="_section">
<MkInfo>{{ $ts.notificationSettingDesc }}</MkInfo>
<MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton>
<MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton>
- <MkSwitch v-for="type in notificationTypes" :key="type" v-model:value="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</MkSwitch>
+ <MkSwitch v-for="type in notificationTypes" :key="type" v-model="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</MkSwitch>
</div>
</div>
</XModalWindow>
diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue
index d4e6b65c70..bce6333d98 100644
--- a/src/client/components/notification.vue
+++ b/src/client/components/notification.vue
@@ -62,8 +62,8 @@ import { defineComponent, markRaw } from 'vue';
import { getNoteSummary } from '@/misc/get-note-summary';
import XReactionIcon from './reaction-icon.vue';
import MkFollowButton from './follow-button.vue';
-import notePage from '../filters/note';
-import { userPage } from '../filters/user';
+import notePage from '@client/filters/note';
+import { userPage } from '@client/filters/user';
import { i18n } from '@client/i18n';
import * as os from '@client/os';
diff --git a/src/client/components/page-preview.vue b/src/client/components/page-preview.vue
index cd896445a7..090c4a6a6c 100644
--- a/src/client/components/page-preview.vue
+++ b/src/client/components/page-preview.vue
@@ -16,7 +16,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import { userName } from '../filters/user';
+import { userName } from '@client/filters/user';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/components/page-window.vue b/src/client/components/page-window.vue
index 26499f7054..c83b040dd8 100644
--- a/src/client/components/page-window.vue
+++ b/src/client/components/page-window.vue
@@ -3,16 +3,12 @@
:initial-width="500"
:initial-height="500"
:can-resize="true"
- :close-right="true"
+ :close-button="false"
:contextmenu="contextmenu"
@closed="$emit('closed')"
>
<template #header>
- <XHeader :info="pageInfo" :with-back="false"/>
- </template>
- <template #buttons>
- <button class="_button" @click="back()" v-if="history.length > 0"><i class="fas fa-chevron-left"></i></button>
- <button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button>
+ <XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()"/>
</template>
<div class="yrolvcoq _flat_">
<component :is="component" v-bind="props" :ref="changePage"/>
@@ -139,6 +135,10 @@ export default defineComponent({
this.navigate(this.history.pop(), false);
},
+ close() {
+ this.$refs.window.close();
+ },
+
expand() {
this.$router.push(this.path);
this.$refs.window.close();
@@ -155,6 +155,5 @@ export default defineComponent({
<style lang="scss" scoped>
.yrolvcoq {
min-height: 100%;
- background: var(--bg);
}
</style>
diff --git a/src/client/components/page/page.number-input.vue b/src/client/components/page/page.number-input.vue
index 1970ee62a9..9c4a537e15 100644
--- a/src/client/components/page/page.number-input.vue
+++ b/src/client/components/page/page.number-input.vue
@@ -1,6 +1,8 @@
<template>
<div>
- <MkInput class="kudkigyw" :value="value" @update:value="updateValue($event)" type="number">{{ hpml.interpolate(block.text) }}</MkInput>
+ <MkInput class="kudkigyw" :model-value="value" @update:modelValue="updateValue($event)" type="number">
+ <template #label>{{ hpml.interpolate(block.text) }}</template>
+ </MkInput>
</div>
</template>
diff --git a/src/client/components/page/page.post.vue b/src/client/components/page/page.post.vue
index 1dfb506d5f..7b061d8cda 100644
--- a/src/client/components/page/page.post.vue
+++ b/src/client/components/page/page.post.vue
@@ -1,6 +1,6 @@
<template>
<div class="ngbfujlo">
- <MkTextarea :value="text" readonly style="margin: 0;"></MkTextarea>
+ <MkTextarea :model-value="text" readonly style="margin: 0;"></MkTextarea>
<MkButton class="button" primary @click="post()" :disabled="posting || posted">
<i v-if="posted" class="fas fa-check"></i>
<i v-else class="fas fa-paper-plane"></i>
diff --git a/src/client/components/page/page.switch.vue b/src/client/components/page/page.switch.vue
index a928c22bee..8818e6cbcf 100644
--- a/src/client/components/page/page.switch.vue
+++ b/src/client/components/page/page.switch.vue
@@ -1,6 +1,6 @@
<template>
<div class="hkcxmtwj">
- <MkSwitch :value="value" @update:value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
+ <MkSwitch :model-value="value" @update:modelValue="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
</div>
</template>
diff --git a/src/client/components/page/page.text-input.vue b/src/client/components/page/page.text-input.vue
index 8bf3e1c88e..752d3d7257 100644
--- a/src/client/components/page/page.text-input.vue
+++ b/src/client/components/page/page.text-input.vue
@@ -1,6 +1,8 @@
<template>
<div>
- <MkInput class="kudkigyw" :value="value" @update:value="updateValue($event)" type="text">{{ hpml.interpolate(block.text) }}</MkInput>
+ <MkInput class="kudkigyw" :model-value="value" @update:modelValue="updateValue($event)" type="text">
+ <template #label>{{ hpml.interpolate(block.text) }}</template>
+ </MkInput>
</div>
</template>
diff --git a/src/client/components/page/page.textarea-input.vue b/src/client/components/page/page.textarea-input.vue
index 9951cef2de..e6cf5117f9 100644
--- a/src/client/components/page/page.textarea-input.vue
+++ b/src/client/components/page/page.textarea-input.vue
@@ -1,6 +1,8 @@
<template>
<div>
- <MkTextarea :value="value" @update:value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkTextarea>
+ <MkTextarea :model-value="value" @update:modelValue="updateValue($event)">
+ <template #label>{{ hpml.interpolate(block.text) }}</template>
+ </MkTextarea>
</div>
</template>
diff --git a/src/client/components/page/page.textarea.vue b/src/client/components/page/page.textarea.vue
index 612bbe41b9..974c7f2c57 100644
--- a/src/client/components/page/page.textarea.vue
+++ b/src/client/components/page/page.textarea.vue
@@ -1,5 +1,5 @@
<template>
-<MkTextarea :value="text" readonly></MkTextarea>
+<MkTextarea :model-value="text" readonly></MkTextarea>
</template>
<script lang="ts">
diff --git a/src/client/components/poll-editor.vue b/src/client/components/poll-editor.vue
index 0ade2c3ba0..dfc198fc1e 100644
--- a/src/client/components/poll-editor.vue
+++ b/src/client/components/poll-editor.vue
@@ -5,8 +5,8 @@
</p>
<ul ref="choices">
<li v-for="(choice, i) in choices" :key="i">
- <MkInput class="input" :value="choice" @update:value="onInput(i, $event)">
- <span>{{ $t('_poll.choiceN', { n: i + 1 }) }}</span>
+ <MkInput class="input" :model-value="choice" @update:modelValue="onInput(i, $event)">
+ <template #label>{{ $t('_poll.choiceN', { n: i + 1 }) }}</template>
</MkInput>
<button @click="remove(i)" class="_button">
<i class="fas fa-times"></i>
@@ -16,27 +16,27 @@
<MkButton class="add" v-if="choices.length < 10" @click="add">{{ $ts.add }}</MkButton>
<MkButton class="add" v-else disabled>{{ $ts._poll.noMore }}</MkButton>
<section>
- <MkSwitch v-model:value="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
+ <MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
<div>
- <MkSelect v-model:value="expiration">
+ <MkSelect v-model="expiration">
<template #label>{{ $ts._poll.expiration }}</template>
<option value="infinite">{{ $ts._poll.infinite }}</option>
<option value="at">{{ $ts._poll.at }}</option>
<option value="after">{{ $ts._poll.after }}</option>
</MkSelect>
<section v-if="expiration === 'at'">
- <MkInput v-model:value="atDate" type="date" class="input">
- <span>{{ $ts._poll.deadlineDate }}</span>
+ <MkInput v-model="atDate" type="date" class="input">
+ <template #label>{{ $ts._poll.deadlineDate }}</template>
</MkInput>
- <MkInput v-model:value="atTime" type="time" class="input">
- <span>{{ $ts._poll.deadlineTime }}</span>
+ <MkInput v-model="atTime" type="time" class="input">
+ <template #label>{{ $ts._poll.deadlineTime }}</template>
</MkInput>
</section>
<section v-if="expiration === 'after'">
- <MkInput v-model:value="after" type="number" class="input">
- <span>{{ $ts._poll.duration }}</span>
+ <MkInput v-model="after" type="number" class="input">
+ <template #label>{{ $ts._poll.duration }}</template>
</MkInput>
- <MkSelect v-model:value="unit">
+ <MkSelect v-model="unit">
<option value="second">{{ $ts._time.second }}</option>
<option value="minute">{{ $ts._time.minute }}</option>
<option value="hour">{{ $ts._time.hour }}</option>
diff --git a/src/client/components/post-form-attaches.vue b/src/client/components/post-form-attaches.vue
index 27e20fdfa8..9365365653 100644
--- a/src/client/components/post-form-attaches.vue
+++ b/src/client/components/post-form-attaches.vue
@@ -112,7 +112,7 @@ export default defineComponent({
showFileMenu(file, ev: MouseEvent) {
if (this.menu) return;
- this.menu = os.modalMenu([{
+ this.menu = os.popupMenu([{
text: this.$ts.renameFile,
icon: 'fas fa-i-cursor',
action: () => { this.rename(file) }
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 13bbb3f9e5..969f8563a4 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -37,6 +37,7 @@
<MkInfo warn v-if="hasNotSpecifiedMentions" class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd" />
+ <input v-show="withHashtags" ref="hashtags" class="hashtags" v-model="hashtags" :placeholder="$ts.hashtags" list="hashtags">
<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
<footer>
@@ -44,9 +45,13 @@
<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$ts.poll"><i class="fas fa-poll-h"></i></button>
<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$ts.useCw"><i class="fas fa-eye-slash"></i></button>
<button class="_button" @click="insertMention" v-tooltip="$ts.mention"><i class="fas fa-at"></i></button>
+ <button class="_button" @click="withHashtags = !withHashtags" v-tooltip="$ts.hashtags"><i class="fas fa-hashtag"></i></button>
<button class="_button" @click="insertEmoji" v-tooltip="$ts.emoji"><i class="fas fa-laugh-squint"></i></button>
<button class="_button" @click="showActions" v-tooltip="$ts.plugin" v-if="postFormActions.length > 0"><i class="fas fa-plug"></i></button>
</footer>
+ <datalist id="hashtags">
+ <option v-for="hashtag in recentHashtags" :value="hashtag" :key="hashtag"/>
+ </datalist>
</div>
</div>
</template>
@@ -67,10 +72,11 @@ import { Autocomplete } from '@client/scripts/autocomplete';
import { noteVisibilities } from '../../types';
import * as os from '@client/os';
import { selectFile } from '@client/scripts/select-file';
-import { notePostInterruptors, postFormActions } from '@client/store';
+import { defaultStore, notePostInterruptors, postFormActions } from '@client/store';
import { isMobile } from '@client/scripts/is-mobile';
import { throttle } from 'throttle-debounce';
import MkInfo from '@client/components/ui/info.vue';
+import { defaultStore } from '@client/store';
export default defineComponent({
components: {
@@ -212,7 +218,10 @@ export default defineComponent({
max(): number {
return this.$instance ? this.$instance.maxNoteTextLength : 1000;
- }
+ },
+
+ withHashtags: defaultStore.makeGetterSetter('postFormWithHashtags'),
+ hashtags: defaultStore.makeGetterSetter('postFormHashtags'),
},
watch: {
@@ -303,6 +312,7 @@ export default defineComponent({
// TODO: detach when unmount
new Autocomplete(this.$refs.text, this, { model: 'text' });
new Autocomplete(this.$refs.cw, this, { model: 'cw' });
+ new Autocomplete(this.$refs.hashtags, this, { model: 'hashtags' });
this.$nextTick(() => {
// 書きかけの投稿を復元
@@ -605,6 +615,11 @@ export default defineComponent({
viaMobile: isMobile
};
+ if (this.withHashtags) {
+ const hashtags = this.hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
+ data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
+ }
+
// plugin
if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) {
@@ -618,8 +633,8 @@ export default defineComponent({
this.$nextTick(() => {
this.deleteDraft();
this.$emit('posted');
- if (this.text && this.text != '') {
- const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
+ if (data.text && data.text != '') {
+ const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
@@ -649,7 +664,7 @@ export default defineComponent({
},
showActions(ev) {
- os.modalMenu(postFormActions.map(action => ({
+ os.popupMenu(postFormActions.map(action => ({
text: action.title,
action: () => {
action.handler({
@@ -785,6 +800,7 @@ export default defineComponent({
}
> .cw,
+ > .hashtags,
> .text {
display: block;
box-sizing: border-box;
@@ -813,6 +829,13 @@ export default defineComponent({
border-bottom: solid 0.5px var(--divider);
}
+ > .hashtags {
+ z-index: 1;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ border-top: solid 0.5px var(--divider);
+ }
+
> .text {
max-width: 100%;
min-width: 100%;
@@ -872,6 +895,7 @@ export default defineComponent({
}
> .cw,
+ > .hashtags,
> .text {
padding: 0 16px;
}
diff --git a/src/client/components/sample.vue b/src/client/components/sample.vue
index 70949ea357..bce02466f6 100644
--- a/src/client/components/sample.vue
+++ b/src/client/components/sample.vue
@@ -1,10 +1,10 @@
<template>
<div class="_card">
<div class="_content">
- <MkInput v-model:value="text">
- <span>Text</span>
+ <MkInput v-model="text">
+ <template #label>Text</template>
</MkInput>
- <MkSwitch v-model:value="flag">
+ <MkSwitch v-model="flag">
<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
</MkSwitch>
<div style="margin: 32px 0;">
@@ -93,7 +93,7 @@ export default defineComponent({
},
async openMenu(ev) {
- os.modalMenu([{
+ os.popupMenu([{
type: 'label',
text: 'Fruits'
}, {
diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue
index f8249ffcd6..f1e5d6afe5 100755
--- a/src/client/components/signin.vue
+++ b/src/client/components/signin.vue
@@ -3,15 +3,13 @@
<div class="auth _section">
<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
<div class="normal-signin" v-if="!totpLogin">
- <MkInput v-model:value="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @update:value="onUsernameChange">
- <span>{{ $ts.username }}</span>
+ <MkInput v-model="username" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @update:modelValue="onUsernameChange">
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
- <MkInput v-model:value="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
- <span>{{ $ts.password }}</span>
+ <MkInput v-model="password" :placeholder="$ts.password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
<template #prefix><i class="fas fa-lock"></i></template>
- <template #desc><button class="_textButton" @click="resetPassword">{{ $ts.forgotPassword }}</button></template>
+ <template #caption><button class="_textButton" @click="resetPassword" type="button">{{ $ts.forgotPassword }}</button></template>
</MkInput>
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
</div>
@@ -27,12 +25,12 @@
</div>
<div class="twofa-group totp-group">
<p style="margin-bottom:0;">{{ $ts.twoStepAuthentication }}</p>
- <MkInput v-model:value="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required>
- <span>{{ $ts.password }}</span>
+ <MkInput v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required>
+ <template #label>{{ $ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template>
</MkInput>
- <MkInput v-model:value="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
- <span>{{ $ts.token }}</span>
+ <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
+ <template #label>{{ $ts.token }}</template>
<template #prefix><i class="fas fa-gavel"></i></template>
</MkInput>
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue
index 671642b291..0cdeb633d8 100644
--- a/src/client/components/signup.vue
+++ b/src/client/components/signup.vue
@@ -1,39 +1,39 @@
<template>
<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<template v-if="meta">
- <MkInput v-if="meta.disableRegistration" v-model:value="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
- <span>{{ $ts.invitationCode }}</span>
+ <MkInput v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
+ <template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template>
</MkInput>
- <MkInput v-model:value="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:value="onChangeUsername">
- <span>{{ $ts.username }}</span>
+ <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername">
+ <template #label>{{ $ts.username }}</template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
- <template #desc>
+ <template #caption>
<span v-if="usernameState == 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
- <span v-if="usernameState == 'ok'" style="color:#3CB7B5"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
- <span v-if="usernameState == 'unavailable'" style="color:#FF1161"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
- <span v-if="usernameState == 'error'" style="color:#FF1161"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
- <span v-if="usernameState == 'invalid-format'" style="color:#FF1161"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
- <span v-if="usernameState == 'min-range'" style="color:#FF1161"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
- <span v-if="usernameState == 'max-range'" style="color:#FF1161"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
+ <span v-if="usernameState == 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
+ <span v-if="usernameState == 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
+ <span v-if="usernameState == 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
+ <span v-if="usernameState == 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
+ <span v-if="usernameState == 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
+ <span v-if="usernameState == 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
</template>
</MkInput>
- <MkInput v-model:value="password" type="password" :autocomplete="Math.random()" required @update:value="onChangePassword">
- <span>{{ $ts.password }}</span>
+ <MkInput v-model="password" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePassword">
+ <template #label>{{ $ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template>
- <template #desc>
- <p v-if="passwordStrength == 'low'" style="color:#FF1161"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</p>
- <p v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</p>
- <p v-if="passwordStrength == 'high'" style="color:#3CB7B5"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</p>
+ <template #caption>
+ <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span>
+ <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span>
+ <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
</template>
</MkInput>
- <MkInput v-model:value="retypedPassword" type="password" :autocomplete="Math.random()" required @update:value="onChangePasswordRetype">
- <span>{{ $ts.password }} ({{ $ts.retype }})</span>
+ <MkInput v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePasswordRetype">
+ <template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
<template #prefix><i class="fas fa-lock"></i></template>
- <template #desc>
- <p v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</p>
- <p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</p>
+ <template #caption>
+ <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span>
+ <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
</template>
</MkInput>
<label v-if="meta.tosUrl" class="tou">
diff --git a/src/client/components/token-generate-window.vue b/src/client/components/token-generate-window.vue
index 87a76931e4..fe61f61efa 100644
--- a/src/client/components/token-generate-window.vue
+++ b/src/client/components/token-generate-window.vue
@@ -14,13 +14,15 @@
<MkInfo warn>{{ information }}</MkInfo>
</div>
<div class="_section">
- <MkInput v-model:value="name">{{ $ts.name }}</MkInput>
+ <MkInput v-model="name">
+ <template #label>{{ $ts.name }}</template>
+ </MkInput>
</div>
<div class="_section">
<div style="margin-bottom: 16px;"><b>{{ $ts.permission }}</b></div>
<MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton>
<MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton>
- <MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model:value="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
+ <MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
</div>
</XModalWindow>
</template>
diff --git a/src/client/components/ui/button.vue b/src/client/components/ui/button.vue
index c92f30db97..4c5d617d76 100644
--- a/src/client/components/ui/button.vue
+++ b/src/client/components/ui/button.vue
@@ -1,6 +1,6 @@
<template>
<component class="bghgjjyj _button"
- :is="link ? 'a' : 'button'"
+ :is="link ? 'MkA' : 'button'"
:class="{ inline, primary, danger, full }"
:type="type"
@click="$emit('click', $event)"
@@ -115,6 +115,7 @@ export default defineComponent({
z-index: 1; // 他コンポーネントのbox-shadowに隠されないようにするため
display: block;
min-width: 100px;
+ width: max-content;
padding: 8px 14px;
text-align: center;
font-weight: normal;
@@ -125,6 +126,7 @@ export default defineComponent({
background: var(--buttonBg);
border-radius: 999px;
overflow: hidden;
+ box-sizing: border-box;
&:not(:disabled):hover {
background: var(--buttonHoverBg);
@@ -140,7 +142,7 @@ export default defineComponent({
&.primary {
font-weight: bold;
- color: #fff !important;
+ color: var(--fgOnAccent) !important;
background: var(--accent);
&:not(:disabled):hover {
diff --git a/src/client/components/ui/folder.vue b/src/client/components/ui/folder.vue
index 4281ec7778..e6af40e36d 100644
--- a/src/client/components/ui/folder.vue
+++ b/src/client/components/ui/folder.vue
@@ -99,9 +99,12 @@ export default defineComponent({
z-index: 10;
position: sticky;
top: var(--stickyTop, 0px);
+ background: var(--panel);
+ /* TODO panelの半透明バージョンをプログラマティックに作りたい
background: var(--X17);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(20px);
+ */
> .title {
margin: 0;
diff --git a/src/client/components/ui/input.vue b/src/client/components/ui/input.vue
index 7415d9896b..05ce5d3e15 100644
--- a/src/client/components/ui/input.vue
+++ b/src/client/components/ui/input.vue
@@ -1,32 +1,9 @@
<template>
-<div class="juejbjww" :class="{ focused, filled, inline, disabled }">
- <div class="icon" ref="icon"><slot name="icon"></slot></div>
- <div class="input">
- <span class="label" ref="labelEl"><slot></slot></span>
- <span class="title" ref="title">
- <slot name="title"></slot>
- <span class="warning" v-if="invalid"><i class="fas fa-exclamation-circle"></i>{{ $refs.input.validationMessage }}</span>
- </span>
+<div class="matxzzsk">
+ <div class="label" @click="focus"><slot name="label"></slot></div>
+ <div class="input" :class="{ inline, disabled, focused }">
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
- <input v-if="debounce" ref="inputEl"
- v-debounce="500"
- :type="type"
- v-model.lazy="v"
- :disabled="disabled"
- :required="required"
- :readonly="readonly"
- :placeholder="placeholder"
- :pattern="pattern"
- :autocomplete="autocomplete"
- :spellcheck="spellcheck"
- :step="step"
- @focus="focused = true"
- @blur="focused = false"
- @keydown="onKeydown($event)"
- @input="onInput"
- :list="id"
- >
- <input v-else ref="inputEl"
+ <input ref="inputEl"
:type="type"
v-model="v"
:disabled="disabled"
@@ -48,23 +25,25 @@
</datalist>
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
</div>
- <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $ts.save }}</button>
- <div class="desc _caption"><slot name="desc"></slot></div>
+ <div class="caption"><slot name="caption"></slot></div>
+
+ <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import debounce from 'v-debounce';
-import * as os from '@client/os';
+import MkButton from './button.vue';
+import { debounce } from 'throttle-debounce';
export default defineComponent({
- directives: {
- debounce
+ components: {
+ MkButton,
},
+
props: {
- value: {
- required: false
+ modelValue: {
+ required: true
},
type: {
type: String,
@@ -104,9 +83,6 @@ export default defineComponent({
step: {
required: false
},
- debounce: {
- required: false
- },
datalist: {
type: Array,
required: false,
@@ -116,15 +92,23 @@ export default defineComponent({
required: false,
default: false
},
- save: {
- type: Function,
+ debounce: {
+ type: Boolean,
required: false,
+ default: false
+ },
+ manualSave: {
+ type: Boolean,
+ required: false,
+ default: false
},
},
- emits: ['change', 'keydown', 'enter'],
+
+ emits: ['change', 'keydown', 'enter', 'update:modelValue'],
+
setup(props, context) {
- const { value, type, autofocus } = toRefs(props);
- const v = ref(value.value);
+ const { modelValue, type, autofocus } = toRefs(props);
+ const v = ref(modelValue.value);
const id = Math.random().toString(); // TODO: uuid?
const focused = ref(false);
const changed = ref(false);
@@ -133,7 +117,6 @@ export default defineComponent({
const inputEl = ref(null);
const prefixEl = ref(null);
const suffixEl = ref(null);
- const labelEl = ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
@@ -148,15 +131,28 @@ export default defineComponent({
}
};
- watch(value, newValue => {
+ const updated = () => {
+ changed.value = false;
+ if (type?.value === 'number') {
+ context.emit('update:modelValue', parseFloat(v.value));
+ } else {
+ context.emit('update:modelValue', v.value);
+ }
+ };
+
+ const debouncedUpdated = debounce(1000, updated);
+
+ watch(modelValue, newValue => {
v.value = newValue;
});
watch(v, newValue => {
- if (type?.value === 'number') {
- context.emit('update:value', parseFloat(newValue));
- } else {
- context.emit('update:value', newValue);
+ if (!props.manualSave) {
+ if (props.debounce) {
+ debouncedUpdated();
+ } else {
+ updated();
+ }
}
invalid.value = inputEl.value.validity.badInput;
@@ -172,7 +168,6 @@ export default defineComponent({
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
const clock = setInterval(() => {
if (prefixEl.value) {
- labelEl.value.style.left = (prefixEl.value.offsetLeft + prefixEl.value.offsetWidth) + 'px';
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
}
@@ -200,148 +195,78 @@ export default defineComponent({
inputEl,
prefixEl,
suffixEl,
- labelEl,
focus,
onInput,
onKeydown,
+ updated,
};
},
});
</script>
<style lang="scss" scoped>
-.juejbjww {
- position: relative;
- margin: 32px 0;
+.matxzzsk {
+ margin: 1.5em 0;
- &:not(.inline):first-child {
- margin-top: 8px;
- }
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 12px;
+ user-select: none;
- &:not(.inline):last-child {
- margin-bottom: 8px;
+ &:empty {
+ display: none;
+ }
}
- > .icon {
- position: absolute;
- top: 0;
- left: 0;
- width: 24px;
- text-align: center;
- line-height: 32px;
+ > .caption {
+ font-size: 0.8em;
+ padding: 8px 0 0 12px;
+ color: var(--fgTransparentWeak);
- &:not(:empty) + .input {
- margin-left: 28px;
+ &:empty {
+ display: none;
}
}
> .input {
+ $height: 42px;
position: relative;
- &:before {
- content: '';
- display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 1px;
- background: var(--inputBorder);
- }
-
- &:after {
- content: '';
- display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 2px;
- background: var(--accent);
- opacity: 0;
- transform: scaleX(0.12);
- transition: border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- will-change: border opacity transform;
- }
-
- > .label {
- position: absolute;
- z-index: 1;
- top: 0;
- left: 0;
- pointer-events: none;
- transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
- transition-duration: 0.3s;
- font-size: 1em;
- line-height: 32px;
- color: var(--inputLabel);
- pointer-events: none;
- //will-change transform
- transform-origin: top left;
- transform: scale(1);
- }
-
- > .title {
- position: absolute;
- z-index: 1;
- top: -17px;
- left: 0 !important;
- pointer-events: none;
- font-size: 1em;
- line-height: 32px;
- color: var(--inputLabel);
- pointer-events: none;
- //will-change transform
- transform-origin: top left;
- transform: scale(.75);
- white-space: nowrap;
- width: 133%;
- overflow: hidden;
- text-overflow: ellipsis;
-
- > .warning {
- margin-left: 0.5em;
- color: var(--infoWarnFg);
-
- > svg {
- margin-right: 0.1em;
- }
- }
- }
-
> input {
- $height: 32px;
+ appearance: none;
+ -webkit-appearance: none;
display: block;
height: $height;
width: 100%;
margin: 0;
- padding: 0;
+ padding: 0 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
- line-height: $height;
- color: var(--inputText);
- background: transparent;
- border: none;
- border-radius: 0;
+ color: var(--fg);
+ background: var(--panel);
+ border: solid 1px var(--inputBorder);
+ border-radius: 6px;
outline: none;
box-shadow: none;
box-sizing: border-box;
+ transition: border-color 0.1s ease-out;
- &[type='file'] {
- display: none;
+ &:hover {
+ border-color: var(--inputBorderHover);
}
}
> .prefix,
> .suffix {
- display: block;
+ display: flex;
+ align-items: center;
position: absolute;
z-index: 1;
top: 0;
+ padding: 0 12px;
font-size: 1em;
- line-height: 32px;
- color: var(--inputLabel);
+ height: $height;
pointer-events: none;
&:empty {
@@ -360,66 +285,32 @@ export default defineComponent({
> .prefix {
left: 0;
- padding-right: 4px;
+ padding-right: 6px;
}
> .suffix {
right: 0;
- padding-left: 4px;
+ padding-left: 6px;
}
- }
- > .save {
- margin: 6px 0 0 0;
- font-size: 0.8em;
- }
-
- > .desc {
- margin: 6px 0 0 0;
-
- &:empty {
- display: none;
- }
-
- * {
+ &.inline {
+ display: inline-block;
margin: 0;
}
- }
-
- &.focused {
- > .input {
- &:after {
- opacity: 1;
- transform: scaleX(1);
- }
-
- > .label {
- color: var(--accent);
- }
- }
- }
- &.focused,
- &.filled {
- > .input {
- > .label {
- top: -17px;
- left: 0 !important;
- transform: scale(0.75);
+ &.focused {
+ > input {
+ border-color: var(--accent);
+ //box-shadow: 0 0 0 4px var(--focus);
}
}
- }
- &.inline {
- display: inline-block;
- margin: 0;
- }
-
- &.disabled {
- opacity: 0.7;
+ &.disabled {
+ opacity: 0.7;
- &, * {
- cursor: not-allowed !important;
+ &, * {
+ cursor: not-allowed !important;
+ }
}
}
}
diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue
index eb96450774..8a1871e256 100644
--- a/src/client/components/ui/menu.vue
+++ b/src/client/components/ui/menu.vue
@@ -171,13 +171,13 @@ export default defineComponent({
}
&:hover {
- color: #fff;
+ color: var(--fgOnAccent);
background: var(--accent);
text-decoration: none;
}
&:active {
- color: #fff;
+ color: var(--fgOnAccent);
background: var(--accentDarken);
}
diff --git a/src/client/components/ui/modal-menu.vue b/src/client/components/ui/popup-menu.vue
index aac4be9c3b..23f7c89f3b 100644
--- a/src/client/components/ui/modal-menu.vue
+++ b/src/client/components/ui/popup-menu.vue
@@ -1,19 +1,20 @@
<template>
-<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
- <MkMenu :items="items" :align="align" @close="$refs.modal.close()" class="_popup"/>
-</MkModal>
+<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
+ <MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup _shadow"/>
+</MkPopup>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import MkModal from './modal.vue';
+import MkPopup from './popup.vue';
import MkMenu from './menu.vue';
export default defineComponent({
components: {
- MkModal,
+ MkPopup,
MkMenu,
},
+
props: {
items: {
type: Array,
@@ -31,17 +32,7 @@ export default defineComponent({
required: false
},
},
- emits: ['closed'],
- computed: {
- keymap(): any {
- return {
- 'esc': () => this.$refs.modal.close(),
- };
- },
- },
+
+ emits: ['close', 'closed'],
});
</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/components/ui/popup.vue b/src/client/components/ui/popup.vue
new file mode 100644
index 0000000000..c98e17fa25
--- /dev/null
+++ b/src/client/components/ui/popup.vue
@@ -0,0 +1,213 @@
+<template>
+<transition :name="$store.state.animation ? 'popup-menu' : ''" :duration="$store.state.animation ? 300 : 0" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered">
+ <div v-show="manualShowing != null ? manualShowing : showing" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" ref="content" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+ <slot></slot>
+ </div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+
+function getFixedContainer(el: Element | null): Element | null {
+ if (el == null || el.tagName === 'BODY') return null;
+ const position = window.getComputedStyle(el).getPropertyValue('position');
+ if (position === 'fixed') {
+ return el;
+ } else {
+ return getFixedContainer(el.parentElement);
+ }
+}
+
+export default defineComponent({
+ props: {
+ manualShowing: {
+ type: Boolean,
+ required: false,
+ default: null,
+ },
+ srcCenter: {
+ type: Boolean,
+ required: false
+ },
+ src: {
+ type: Object as PropType<HTMLElement>,
+ required: false,
+ },
+ position: {
+ required: false
+ },
+ front: {
+ type: Boolean,
+ required: false,
+ default: false,
+ }
+ },
+
+ emits: ['opening', 'click', 'esc', 'close', 'closed'],
+
+ data() {
+ return {
+ showing: true,
+ fixed: false,
+ transformOrigin: 'center',
+ contentClicking: false,
+ };
+ },
+
+ mounted() {
+ this.$watch('src', () => {
+ if (this.src) {
+ this.src.style.pointerEvents = 'none';
+ }
+ this.fixed = getFixedContainer(this.src) != null;
+ this.$nextTick(() => {
+ this.align();
+ });
+ }, { immediate: true });
+
+ this.$nextTick(() => {
+ const popover = this.$refs.content as any;
+ new ResizeObserver((entries, observer) => {
+ this.align();
+ }).observe(popover);
+ });
+
+ document.addEventListener('mousedown', this.onDocumentClick, { passive: true });
+ },
+
+ beforeUnmount() {
+ document.removeEventListener('mousedown', this.onDocumentClick);
+ },
+
+ methods: {
+ align() {
+ if (this.src == null) return;
+
+ const popover = this.$refs.content as any;
+
+ if (popover == null) return;
+
+ const rect = this.src.getBoundingClientRect();
+
+ const width = popover.offsetWidth;
+ const height = popover.offsetHeight;
+
+ let left;
+ let top;
+
+ if (this.srcCenter) {
+ const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
+ const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2);
+ left = (x - (width / 2));
+ top = (y - (height / 2));
+ } else {
+ const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
+ const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight;
+ left = (x - (width / 2));
+ top = y;
+ }
+
+ if (this.fixed) {
+ if (left + width > window.innerWidth) {
+ left = window.innerWidth - width;
+ }
+
+ if (top + height > window.innerHeight) {
+ top = window.innerHeight - height;
+ }
+ } else {
+ if (left + width - window.pageXOffset > window.innerWidth) {
+ left = window.innerWidth - width + window.pageXOffset - 1;
+ }
+
+ if (top + height - window.pageYOffset > window.innerHeight) {
+ top = window.innerHeight - height + window.pageYOffset - 1;
+ }
+ }
+
+ if (top < 0) {
+ top = 0;
+ }
+
+ if (left < 0) {
+ left = 0;
+ }
+
+ if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) {
+ this.transformOrigin = 'center top';
+ } else {
+ this.transformOrigin = 'center';
+ }
+
+ popover.style.left = left + 'px';
+ popover.style.top = top + 'px';
+ },
+
+ childRendered() {
+ // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
+ const content = this.$refs.content.children[0];
+ content.addEventListener('mousedown', e => {
+ this.contentClicking = true;
+ window.addEventListener('mouseup', e => {
+ // click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
+ setTimeout(() => {
+ this.contentClicking = false;
+ }, 100);
+ }, { passive: true, once: true });
+ }, { passive: true });
+ },
+
+ close() {
+ if (this.src) this.src.style.pointerEvents = 'auto';
+ this.showing = false;
+ this.$emit('close');
+ },
+
+ onClosed() {
+ this.$emit('closed');
+ },
+
+ onDocumentClick(ev) {
+ const flyoutElement = this.$refs.content;
+ let targetElement = ev.target;
+ do {
+ if (targetElement === flyoutElement) {
+ return;
+ }
+ targetElement = targetElement.parentNode;
+ } while (targetElement);
+ this.close();
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.popup-menu-enter-active {
+ transform-origin: var(--transformOrigin);
+ transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
+}
+.popup-menu-leave-active {
+ transform-origin: var(--transformOrigin);
+ transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), transform 0.2s cubic-bezier(0.4, 0, 1, 1) !important;
+}
+.popup-menu-enter-from, .popup-menu-leave-to {
+ pointer-events: none;
+ opacity: 0;
+ transform: scale(0.9);
+}
+
+.ccczpooj {
+ position: absolute;
+ z-index: 10000;
+
+ &.fixed {
+ position: fixed;
+ }
+
+ &.front {
+ z-index: 20000;
+ }
+}
+</style>
diff --git a/src/client/components/ui/select.vue b/src/client/components/ui/select.vue
index e78c44fe0d..e9d43d8a64 100644
--- a/src/client/components/ui/select.vue
+++ b/src/client/components/ui/select.vue
@@ -1,185 +1,218 @@
<template>
-<div class="eiipwacr" :class="{ focused, disabled, filled, inline }">
- <div class="icon" ref="icon"><slot name="icon"></slot></div>
- <div class="input" @click="focus">
- <span class="label" ref="label"><slot name="label"></slot></span>
- <div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
- <select ref="input"
+<div class="vblkjoeq">
+ <div class="label" @click="focus"><slot name="label"></slot></div>
+ <div class="input" :class="{ inline, disabled, focused }">
+ <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+ <select ref="inputEl"
v-model="v"
- :required="required"
:disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ :placeholder="placeholder"
@focus="focused = true"
@blur="focused = false"
+ @input="onInput"
>
<slot></slot>
</select>
- <div class="suffix">
- <slot name="suffix">
- <i class="fas fa-chevron-down"></i>
- </slot>
- </div>
+ <div class="suffix" ref="suffixEl"><i class="fas fa-chevron-down"></i></div>
</div>
- <div class="text"><slot name="text"></slot></div>
+ <div class="caption"><slot name="caption"></slot></div>
+
+ <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</div>
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+import MkButton from './button.vue';
export default defineComponent({
+ components: {
+ MkButton,
+ },
+
props: {
- value: {
- required: false
+ modelValue: {
+ required: true
},
required: {
type: Boolean,
required: false
},
+ readonly: {
+ type: Boolean,
+ required: false
+ },
disabled: {
type: Boolean,
required: false
},
+ placeholder: {
+ type: String,
+ required: false
+ },
+ autofocus: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
inline: {
type: Boolean,
required: false,
default: false
},
+ manualSave: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
},
- data() {
- return {
- focused: false,
+
+ emits: ['change', 'update:modelValue'],
+
+ setup(props, context) {
+ const { modelValue, autofocus } = toRefs(props);
+ const v = ref(modelValue.value);
+ const focused = ref(false);
+ const changed = ref(false);
+ const invalid = ref(false);
+ const filled = computed(() => v.value !== '' && v.value != null);
+ const inputEl = ref(null);
+ const prefixEl = ref(null);
+ const suffixEl = ref(null);
+
+ const focus = () => inputEl.value.focus();
+ const onInput = (ev) => {
+ changed.value = true;
+ context.emit('change', ev);
};
- },
- computed: {
- v: {
- get() {
- return this.value;
- },
- set(v) {
- this.$emit('update:value', v);
+
+ const updated = () => {
+ changed.value = false;
+ context.emit('update:modelValue', v.value);
+ };
+
+ watch(modelValue, newValue => {
+ v.value = newValue;
+ });
+
+ watch(v, newValue => {
+ if (!props.manualSave) {
+ updated();
}
- },
- filled(): boolean {
- return true;
- }
- },
- mounted() {
- if (this.$refs.prefix) {
- this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
- }
+
+ invalid.value = inputEl.value.validity.badInput;
+ });
+
+ onMounted(() => {
+ nextTick(() => {
+ if (autofocus.value) {
+ focus();
+ }
+
+ // このコンポーネントが作成された時、非表示状態である場合がある
+ // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
+ const clock = setInterval(() => {
+ if (prefixEl.value) {
+ if (prefixEl.value.offsetWidth) {
+ inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
+ }
+ }
+ if (suffixEl.value) {
+ if (suffixEl.value.offsetWidth) {
+ inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
+ }
+ }
+ }, 100);
+
+ onUnmounted(() => {
+ clearInterval(clock);
+ });
+ });
+ });
+
+ return {
+ v,
+ focused,
+ invalid,
+ changed,
+ filled,
+ inputEl,
+ prefixEl,
+ suffixEl,
+ focus,
+ onInput,
+ updated,
+ };
},
- methods: {
- focus() {
- this.$refs.input.focus();
- }
- }
});
</script>
<style lang="scss" scoped>
-.eiipwacr {
- position: relative;
- margin: 32px 0;
+.vblkjoeq {
+ margin: 1.5em 0;
- &:not(.inline):first-child {
- margin-top: 8px;
- }
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 12px;
+ user-select: none;
- &:not(.inline):last-child {
- margin-bottom: 8px;
+ &:empty {
+ display: none;
+ }
}
- > .icon {
- position: absolute;
- top: 0;
- left: 0;
- width: 24px;
- text-align: center;
- line-height: 32px;
+ > .caption {
+ font-size: 0.8em;
+ padding: 8px 0 0 12px;
+ color: var(--fgTransparentWeak);
- &:not(:empty) + .input {
- margin-left: 28px;
+ &:empty {
+ display: none;
}
}
> .input {
- display: flex;
+ $height: 42px;
position: relative;
- &:before {
- content: '';
- display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 1px;
- background: var(--inputBorder);
- }
-
- &:after {
- content: '';
- display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 2px;
- background: var(--accent);
- opacity: 0;
- transform: scaleX(0.12);
- transition: border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- will-change: border opacity transform;
- }
-
- > .label {
- position: absolute;
- top: 0;
- left: 0;
- pointer-events: none;
- transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
- transition-duration: 0.3s;
- font-size: 1em;
- line-height: 32px;
- pointer-events: none;
- //will-change transform
- transform-origin: top left;
- transform: scale(1);
- }
-
> select {
+ appearance: none;
+ -webkit-appearance: none;
display: block;
- flex: 1;
+ height: $height;
width: 100%;
- padding: 0;
+ margin: 0;
+ padding: 0 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
- height: 32px;
- background: none;
- border: none;
- border-radius: 0;
+ color: var(--fg);
+ background: var(--panel);
+ border: solid 1px var(--inputBorder);
+ border-radius: 6px;
outline: none;
box-shadow: none;
- appearance: none;
- -webkit-appearance: none;
- color: var(--fg);
+ box-sizing: border-box;
+ cursor: pointer;
+ transition: border-color 0.1s ease-out;
- option,
- optgroup {
- color: var(--fg);
- background: var(--bg);
+ &:hover {
+ border-color: var(--inputBorderHover);
}
}
> .prefix,
> .suffix {
- display: block;
- align-self: center;
- justify-self: center;
+ display: flex;
+ align-items: center;
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ padding: 0 12px;
font-size: 1em;
- line-height: 32px;
- color: var(--inputLabel);
+ height: $height;
pointer-events: none;
&:empty {
@@ -187,53 +220,41 @@ export default defineComponent({
}
> * {
- display: block;
+ display: inline-block;
min-width: 16px;
+ max-width: 150px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
}
}
> .prefix {
- padding-right: 4px;
+ left: 0;
+ padding-right: 6px;
}
> .suffix {
- padding-left: 4px;
- }
- }
-
- > .text {
- margin: 6px 0;
- font-size: 0.8em;
-
- &:empty {
- display: none;
+ right: 0;
+ padding-left: 6px;
}
- * {
+ &.inline {
+ display: inline-block;
margin: 0;
}
- }
-
- &.focused {
- > .input {
- &:after {
- opacity: 1;
- transform: scaleX(1);
- }
- > .label {
- color: var(--accent);
+ &.focused {
+ > select {
+ border-color: var(--accent);
}
}
- }
- &.focused,
- &.filled {
- > .input {
- > .label {
- top: -17px;
- left: 0 !important;
- transform: scale(0.75);
+ &.disabled {
+ opacity: 0.7;
+
+ &, * {
+ cursor: not-allowed !important;
}
}
}
diff --git a/src/client/components/ui/switch.vue b/src/client/components/ui/switch.vue
index 762fba6d99..7aa9c0619d 100644
--- a/src/client/components/ui/switch.vue
+++ b/src/client/components/ui/switch.vue
@@ -18,7 +18,7 @@
</span>
<span class="label">
<span><slot></slot></span>
- <p><slot name="desc"></slot></p>
+ <p><slot name="caption"></slot></p>
</span>
</div>
</template>
@@ -28,7 +28,7 @@ import { defineComponent } from 'vue';
export default defineComponent({
props: {
- value: {
+ modelValue: {
type: Boolean,
default: false
},
@@ -39,13 +39,13 @@ export default defineComponent({
},
computed: {
checked(): boolean {
- return this.value;
+ return this.modelValue;
}
},
methods: {
toggle() {
if (this.disabled) return;
- this.$emit('update:value', !this.checked);
+ this.$emit('update:modelValue', !this.checked);
}
}
});
@@ -136,7 +136,7 @@ export default defineComponent({
> p {
margin: 0;
- opacity: 0.7;
+ color: var(--fgTransparentWeak);
font-size: 90%;
}
}
diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue
index 1032c10d14..53a141f011 100644
--- a/src/client/components/ui/textarea.vue
+++ b/src/client/components/ui/textarea.vue
@@ -1,30 +1,45 @@
<template>
-<div class="adhpbeos" :class="{ focused, filled, tall, pre }">
- <div class="input">
- <span class="label" ref="label"><slot></slot></span>
- <textarea ref="input" :class="{ code, _monospace: code }"
- :value="value"
+<div class="adhpbeos">
+ <div class="label" @click="focus"><slot name="label"></slot></div>
+ <div class="input" :class="{ disabled, focused, tall, pre }">
+ <textarea ref="inputEl"
+ :class="{ code, _monospace: code }"
+ v-model="v"
+ :disabled="disabled"
:required="required"
:readonly="readonly"
+ :placeholder="placeholder"
:pattern="pattern"
:autocomplete="autocomplete"
- :spellcheck="!code"
- @input="onInput"
+ :spellcheck="spellcheck"
@focus="focused = true"
@blur="focused = false"
+ @keydown="onKeydown($event)"
+ @input="onInput"
></textarea>
</div>
- <button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $ts.save }}</button>
- <div class="desc _caption"><slot name="desc"></slot></div>
+ <div class="caption"><slot name="caption"></slot></div>
+
+ <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</div>
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+import MkButton from './button.vue';
+import { debounce } from 'throttle-debounce';
export default defineComponent({
+ components: {
+ MkButton,
+ },
+
props: {
- value: {
+ modelValue: {
+ required: true
+ },
+ type: {
+ type: String,
required: false
},
required: {
@@ -35,14 +50,29 @@ export default defineComponent({
type: Boolean,
required: false
},
+ disabled: {
+ type: Boolean,
+ required: false
+ },
pattern: {
type: String,
required: false
},
- autocomplete: {
+ placeholder: {
type: String,
required: false
},
+ autofocus: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ autocomplete: {
+ required: false
+ },
+ spellcheck: {
+ required: false
+ },
code: {
type: Boolean,
required: false
@@ -57,169 +87,164 @@ export default defineComponent({
required: false,
default: false
},
- save: {
- type: Function,
+ debounce: {
+ type: Boolean,
required: false,
+ default: false
+ },
+ manualSave: {
+ type: Boolean,
+ required: false,
+ default: false
},
},
- data() {
+
+ emits: ['change', 'keydown', 'enter', 'update:modelValue'],
+
+ setup(props, context) {
+ const { modelValue, autofocus } = toRefs(props);
+ const v = ref(modelValue.value);
+ const focused = ref(false);
+ const changed = ref(false);
+ const invalid = ref(false);
+ const filled = computed(() => v.value !== '' && v.value != null);
+ const inputEl = ref(null);
+
+ const focus = () => inputEl.value.focus();
+ const onInput = (ev) => {
+ changed.value = true;
+ context.emit('change', ev);
+ };
+ const onKeydown = (ev: KeyboardEvent) => {
+ context.emit('keydown', ev);
+
+ if (ev.code === 'Enter') {
+ context.emit('enter');
+ }
+ };
+
+ const updated = () => {
+ changed.value = false;
+ context.emit('update:modelValue', v.value);
+ };
+
+ const debouncedUpdated = debounce(1000, updated);
+
+ watch(modelValue, newValue => {
+ v.value = newValue;
+ });
+
+ watch(v, newValue => {
+ if (!props.manualSave) {
+ if (props.debounce) {
+ debouncedUpdated();
+ } else {
+ updated();
+ }
+ }
+
+ invalid.value = inputEl.value.validity.badInput;
+ });
+
+ onMounted(() => {
+ nextTick(() => {
+ if (autofocus.value) {
+ focus();
+ }
+ });
+ });
+
return {
- focused: false,
- changed: false,
- }
+ v,
+ focused,
+ invalid,
+ changed,
+ filled,
+ inputEl,
+ focus,
+ onInput,
+ onKeydown,
+ updated,
+ };
},
- computed: {
- filled(): boolean {
- return this.value != '' && this.value != null;
- }
- },
- methods: {
- focus() {
- this.$refs.input.focus();
- },
- onInput(ev) {
- this.changed = true;
- this.$emit('update:value', ev.target.value);
- }
- }
});
</script>
<style lang="scss" scoped>
.adhpbeos {
- margin: 42px 0 32px 0;
- position: relative;
+ margin: 1.5em 0;
- &:first-child {
- margin-top: 16px;
- }
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 12px;
+ user-select: none;
- &:last-child {
- margin-bottom: 0;
+ &:empty {
+ display: none;
+ }
}
- > .input {
- position: relative;
-
- &:before {
- content: '';
- display: block;
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- background: none;
- border: solid 1px var(--inputBorder);
- border-radius: 3px;
- pointer-events: none;
- }
+ > .caption {
+ font-size: 0.8em;
+ padding: 8px 0 0 12px;
+ color: var(--fgTransparentWeak);
- &:after {
- content: '';
- display: block;
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- background: none;
- border: solid 2px var(--accent);
- border-radius: 3px;
- opacity: 0;
- transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- pointer-events: none;
+ &:empty {
+ display: none;
}
+ }
- > .label {
- position: absolute;
- top: 6px;
- left: 12px;
- pointer-events: none;
- transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
- transition-duration: 0.3s;
- font-size: 1em;
- line-height: 32px;
- pointer-events: none;
- //will-change transform
- transform-origin: top left;
- transform: scale(1);
- }
+ > .input {
+ position: relative;
> textarea {
+ appearance: none;
+ -webkit-appearance: none;
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
min-height: 130px;
+ margin: 0;
padding: 12px;
- box-sizing: border-box;
font: inherit;
font-weight: normal;
font-size: 1em;
- background: transparent;
- border: none;
- border-radius: 0;
+ color: var(--fg);
+ background: var(--panel);
+ border: solid 1px var(--inputBorder);
+ border-radius: 6px;
outline: none;
box-shadow: none;
- color: var(--fg);
+ box-sizing: border-box;
+ transition: border-color 0.1s ease-out;
- &.code {
- tab-size: 2;
+ &:hover {
+ border-color: var(--inputBorderHover);
}
}
- }
-
- > .save {
- margin: 6px 0 0 0;
- font-size: 0.8em;
- }
-
- > .desc {
- margin: 6px 0 0 0;
-
- &:empty {
- display: none;
- }
- * {
- margin: 0;
- }
- }
-
- &.focused {
- > .input {
- &:after {
- opacity: 1;
- }
-
- > .label {
- color: var(--accent);
+ &.focused {
+ > textarea {
+ border-color: var(--accent);
}
}
- }
- &.focused,
- &.filled {
- > .input {
- > .label {
- top: -24px;
- left: 0 !important;
- transform: scale(0.75);
+ &.disabled {
+ opacity: 0.7;
+
+ &, * {
+ cursor: not-allowed !important;
}
}
- }
- &.tall {
- > .input {
+ &.tall {
> textarea {
min-height: 200px;
}
}
- }
- &.pre {
- > .input {
+ &.pre {
> textarea {
white-space: pre;
}
diff --git a/src/client/components/ui/window.vue b/src/client/components/ui/window.vue
index ce621ac6fd..f8b7d82d4a 100644
--- a/src/client/components/ui/window.vue
+++ b/src/client/components/ui/window.vue
@@ -3,15 +3,11 @@
<div class="ebkgocck" :class="{ front }" v-if="showing">
<div class="body _popup _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
- <slot v-if="closeRight" name="buttons"><button class="_button" style="pointer-events: none;"></button></slot>
- <button v-else class="_button" @click="close()"><i class="fas fa-times"></i></button>
+ <button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button>
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
<slot name="header"></slot>
</span>
-
- <button v-if="closeRight" class="_button" @click="close()"><i class="fas fa-times"></i></button>
- <slot v-else name="buttons"><button class="_button" style="pointer-events: none;"></button></slot>
</div>
<div class="body" v-if="padding">
<div class="_section">
@@ -86,10 +82,10 @@ export default defineComponent({
required: false,
default: false,
},
- closeRight: {
+ closeButton: {
type: Boolean,
required: false,
- default: false,
+ default: true,
},
mini: {
type: Boolean,
diff --git a/src/client/components/user-info.vue b/src/client/components/user-info.vue
index 402aa0d07c..e76f2ecaa6 100644
--- a/src/client/components/user-info.vue
+++ b/src/client/components/user-info.vue
@@ -31,7 +31,7 @@
import { defineComponent } from 'vue';
import { parseAcct } from '@/misc/acct';
import MkFollowButton from './follow-button.vue';
-import { userPage } from '../filters/user';
+import { userPage } from '@client/filters/user';
export default defineComponent({
components: {
diff --git a/src/client/components/user-list.vue b/src/client/components/user-list.vue
index a7162ddcc2..9c91183971 100644
--- a/src/client/components/user-list.vue
+++ b/src/client/components/user-list.vue
@@ -18,7 +18,7 @@
import { defineComponent } from 'vue';
import paging from '@client/scripts/paging';
import MkUserInfo from './user-info.vue';
-import { userPage } from '../filters/user';
+import { userPage } from '@client/filters/user';
export default defineComponent({
components: {
diff --git a/src/client/components/user-preview.vue b/src/client/components/user-preview.vue
index a495266894..1249f205aa 100644
--- a/src/client/components/user-preview.vue
+++ b/src/client/components/user-preview.vue
@@ -35,7 +35,7 @@
import { defineComponent } from 'vue';
import { parseAcct } from '@/misc/acct';
import MkFollowButton from './follow-button.vue';
-import { userPage } from '../filters/user';
+import { userPage } from '@client/filters/user';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/components/user-select-dialog.vue b/src/client/components/user-select-dialog.vue
index 74081753b7..87c32dab25 100644
--- a/src/client/components/user-select-dialog.vue
+++ b/src/client/components/user-select-dialog.vue
@@ -10,9 +10,15 @@
<template #header>{{ $ts.selectUser }}</template>
<div class="tbhwbxda _monolithic_">
<div class="_section">
- <div class="inputs">
- <MkInput v-model:value="username" class="input" @update:value="search" ref="username"><span>{{ $ts.username }}</span><template #prefix>@</template></MkInput>
- <MkInput v-model:value="host" class="input" @update:value="search"><span>{{ $ts.host }}</span><template #prefix>@</template></MkInput>
+ <div class="_inputSplit _inputNoTopMargin _inputNoBottomMargin">
+ <MkInput v-model="username" class="input" @update:modelValue="search" ref="username">
+ <template #label>{{ $ts.username }}</template>
+ <template #prefix>@</template>
+ </MkInput>
+ <MkInput v-model="host" class="input" @update:modelValue="search">
+ <template #label>{{ $ts.host }}</template>
+ <template #prefix>@</template>
+ </MkInput>
</div>
</div>
<div class="_section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }">
@@ -138,14 +144,6 @@ export default defineComponent({
padding: 0;
}
- > .inputs {
- > .input {
- display: inline-block;
- width: 50%;
- margin: 0;
- }
- }
-
> .users {
flex: 1;
overflow: auto;
diff --git a/src/client/components/users-dialog.vue b/src/client/components/users-dialog.vue
index 90cd926f0c..5199f34c14 100644
--- a/src/client/components/users-dialog.vue
+++ b/src/client/components/users-dialog.vue
@@ -28,7 +28,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import paging from '@client/scripts/paging';
-import { userPage } from '../filters/user';
+import { userPage } from '@client/filters/user';
export default defineComponent({
mixins: [
diff --git a/src/client/components/widgets.vue b/src/client/components/widgets.vue
index 0baef86565..6e5c2d5ade 100644
--- a/src/client/components/widgets.vue
+++ b/src/client/components/widgets.vue
@@ -2,7 +2,7 @@
<div class="vjoppmmu">
<template v-if="edit">
<header>
- <MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)">
+ <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
<template #label>{{ $ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
</MkSelect>