Improve input dialog

This commit is contained in:
syuilo 2018-12-02 20:10:53 +09:00
parent 2b824f7169
commit b69cce2f87
26 changed files with 201 additions and 318 deletions

View File

@ -52,8 +52,8 @@ export default Vue.extend({
type: 'warning', type: 'warning',
text: this.$t('_remove.are-you-sure').replace('$1', this.announcements.find((_, j) => j == i).title), text: this.$t('_remove.are-you-sure').replace('$1', this.announcements.find((_, j) => j == i).title),
showCancelButton: true showCancelButton: true
}).then(res => { }).then(({ canceled }) => {
if (!res) return; if (canceled) return;
this.announcements = this.announcements.filter((_, j) => j !== i); this.announcements = this.announcements.filter((_, j) => j !== i);
this.save(true); this.save(true);
this.$root.dialog({ this.$root.dialog({

View File

@ -120,8 +120,8 @@ export default Vue.extend({
type: 'warning', type: 'warning',
text: this.$t('remove-emoji.are-you-sure').replace('$1', emoji.name), text: this.$t('remove-emoji.are-you-sure').replace('$1', emoji.name),
showCancelButton: true showCancelButton: true
}).then(res => { }).then(({ canceled }) => {
if (!res) return; if (canceled) return;
this.$root.api('admin/emoji/remove', { this.$root.api('admin/emoji/remove', {
id: emoji.id id: emoji.id

View File

@ -50,10 +50,13 @@ export default Vue.extend({
methods: { methods: {
regenerateToken() { regenerateToken() {
this.$input({ this.$root.dialog({
title: this.$t('enter-password'), title: this.$t('enter-password'),
type: 'password' input: {
}).then(password => { type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
this.$root.api('i/regenerate_token', { this.$root.api('i/regenerate_token', {
password: password password: password
}); });

View File

@ -2,15 +2,17 @@
<div class="felqjxyj" :class="{ splash }"> <div class="felqjxyj" :class="{ splash }">
<div class="bg" ref="bg" @click="onBgClick"></div> <div class="bg" ref="bg" @click="onBgClick"></div>
<div class="main" ref="main"> <div class="main" ref="main">
<div class="icon" v-if="type" :class="type"><fa :icon="icon"/></div> <div class="icon" v-if="!input && !select && !user" :class="type"><fa :icon="icon"/></div>
<header v-if="title" v-html="title"></header> <header v-if="title" v-html="title"></header>
<div class="body" v-if="text" v-html="text"></div> <div class="body" v-if="text" v-html="text"></div>
<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><span slot="prefix">@</span></ui-input>
<ui-select v-if="select" v-model="selectedValue"> <ui-select v-if="select" v-model="selectedValue">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option> <option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
</ui-select> </ui-select>
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash"> <ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash">
<ui-button @click="ok" primary autofocus>OK</ui-button> <ui-button @click="ok" primary :autofocus="!input && !select && !user">OK</ui-button>
<ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button> <ui-button @click="cancel" v-if="showCancelButton || input || select || user">Cancel</ui-button>
</ui-horizon-group> </ui-horizon-group>
</div> </div>
</div> </div>
@ -20,6 +22,7 @@
import Vue from 'vue'; import Vue from 'vue';
import * as anime from 'animejs'; import * as anime from 'animejs';
import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons'; import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
import parseAcct from "../../../../../misc/acct/parse";
export default Vue.extend({ export default Vue.extend({
props: { props: {
@ -36,9 +39,15 @@ export default Vue.extend({
type: String, type: String,
required: false required: false
}, },
input: {
required: false
},
select: { select: {
required: false required: false
}, },
user: {
required: false
},
showCancelButton: { showCancelButton: {
type: Boolean, type: Boolean,
default: false default: false
@ -51,6 +60,8 @@ export default Vue.extend({
data() { data() {
return { return {
inputValue: this.input && this.input.default ? this.input.default : null,
userInputValue: null,
selectedValue: null selectedValue: null
}; };
}, },
@ -94,10 +105,21 @@ export default Vue.extend({
}, },
methods: { methods: {
ok() { async ok() {
const result = this.select ? this.selectedValue : true; if (this.user) {
this.$emit('ok', result); const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
this.close(); if (user) {
this.$emit('ok', user);
this.close();
}
} else {
const result =
this.input ? this.inputValue :
this.select ? this.selectedValue :
true;
this.$emit('ok', result);
this.close();
}
}, },
cancel() { cancel() {
@ -127,6 +149,14 @@ export default Vue.extend({
onBgClick() { onBgClick() {
this.cancel(); this.cancel();
},
onInputKeydown(e) {
if (e.which == 13) { // Enter
e.preventDefault();
e.stopPropagation();
this.ok();
}
} }
} }
}); });

View File

@ -99,23 +99,22 @@ export default Vue.extend({
this.$emit('go', game); this.$emit('go', game);
}, },
match() { async match() {
this.$input({ const { result: user } = await this.$root.dialog({
title: this.$t('enter-username') title: this.$t('enter-username'),
}).then(username => { user: {
this.$root.api('users/show', { local: true
username }
}).then(user => { });
this.$root.api('games/reversi/match', { if (user == null) return;
userId: user.id this.$root.api('games/reversi/match', {
}).then(res => { userId: user.id
if (res == null) { }).then(res => {
this.$emit('matching', user); if (res == null) {
} else { this.$emit('matching', user);
this.$emit('go', res); } else {
} this.$emit('go', res);
}); }
});
}); });
}, },

View File

@ -99,8 +99,8 @@ export default Vue.extend({
type: 'warning', type: 'warning',
text: this.$t('delete-confirm'), text: this.$t('delete-confirm'),
showCancelButton: true showCancelButton: true
}).then(res => { }).then(({ canceled }) => {
if (!res) return; if (canceled) return;
this.$root.api('notes/delete', { this.$root.api('notes/delete', {
noteId: this.note.id noteId: this.note.id

View File

@ -11,34 +11,43 @@ import i18n from '../../../i18n';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('common/views/components/password-settings.vue'), i18n: i18n('common/views/components/password-settings.vue'),
methods: { methods: {
reset() { async reset() {
this.$input({ const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({
title: this.$t('enter-current-password'), title: this.$t('enter-current-password'),
type: 'password' input: {
}).then(currentPassword => {
this.$input({
title: this.$t('enter-new-password'),
type: 'password' type: 'password'
}).then(newPassword => { }
this.$input({ });
title: this.$t('enter-new-password-again'), if (canceled1) return;
type: 'password'
}).then(newPassword2 => { const { canceled: canceled2, result: newPassword } = await this.$root.dialog({
if (newPassword !== newPassword2) { title: this.$t('enter-new-password'),
this.$root.dialog({ input: {
title: null, type: 'password'
text: this.$t('not-match') }
}); });
return; if (canceled2) return;
}
this.$root.api('i/change_password', { const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({
currentPasword: currentPassword, title: this.$t('enter-new-password-again'),
newPassword: newPassword input: {
}).then(() => { type: 'password'
this.$notify(this.$t('changed')); }
}); });
}); if (canceled3) return;
if (newPassword !== newPassword2) {
this.$root.dialog({
title: null,
text: this.$t('not-match')
}); });
return;
}
this.$root.api('i/change_password', {
currentPasword: currentPassword,
newPassword: newPassword
}).then(() => {
this.$notify(this.$t('changed'));
}); });
} }
} }

View File

@ -222,10 +222,13 @@ export default Vue.extend({
}, },
updateEmail() { updateEmail() {
this.$input({ this.$root.dialog({
title: this.$t('@.enter-password'), title: this.$t('@.enter-password'),
type: 'password' input: {
}).then(password => { type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
this.$root.api('i/update_email', { this.$root.api('i/update_email', {
password: password, password: password,
email: this.email == '' ? null : this.email email: this.email == '' ? null : this.email

View File

@ -14,17 +14,19 @@
:disabled="disabled" :disabled="disabled"
:required="required" :required="required"
:readonly="readonly" :readonly="readonly"
:placeholder="placeholder"
:pattern="pattern" :pattern="pattern"
:autocomplete="autocomplete" :autocomplete="autocomplete"
:spellcheck="spellcheck" :spellcheck="spellcheck"
@focus="focused = true" @focus="focused = true"
@blur="focused = false" @blur="focused = false"
@keydown="$emit('keydown', $event)"
> >
</template> </template>
<template v-else> <template v-else>
<input ref="input" <input ref="input"
type="text" type="text"
:value="placeholder" :value="filePlaceholder"
readonly readonly
@click="chooseFile" @click="chooseFile"
> >
@ -74,6 +76,15 @@ export default Vue.extend({
type: String, type: String,
required: false required: false
}, },
placeholder: {
type: String,
required: false
},
autofocus: {
type: Boolean,
required: false,
default: false
},
autocomplete: { autocomplete: {
required: false required: false
}, },
@ -109,7 +120,7 @@ export default Vue.extend({
filled(): boolean { filled(): boolean {
return this.v != '' && this.v != null; return this.v != '' && this.v != null;
}, },
placeholder(): string { filePlaceholder(): string {
if (this.type != 'file') return null; if (this.type != 'file') return null;
if (this.v == null) return null; if (this.v == null) return null;
@ -142,6 +153,12 @@ export default Vue.extend({
} }
}, },
mounted() { mounted() {
if (this.autofocus) {
this.$nextTick(() => {
this.$refs.input.focus();
});
}
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.prefix) { if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px'; this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';

View File

@ -34,7 +34,6 @@ import PostFormWindow from './views/components/post-form-window.vue';
import RenoteFormWindow from './views/components/renote-form-window.vue'; import RenoteFormWindow from './views/components/renote-form-window.vue';
import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue'; import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue';
import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue'; import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue';
import InputDialog from './views/components/input-dialog.vue';
import Notification from './views/components/ui-notification.vue'; import Notification from './views/components/ui-notification.vue';
import { url } from '../config'; import { url } from '../config';
@ -113,22 +112,6 @@ init(async (launch) => {
}); });
}, },
$input(opts) {
return new Promise<string>((res, rej) => {
const o = opts || {};
const d = this.$root.new(InputDialog, {
title: o.title,
placeholder: o.placeholder,
default: o.default,
type: o.type || 'text',
allowEmpty: o.allowEmpty
});
d.$once('done', text => {
res(text);
});
});
},
$notify(message) { $notify(message) {
this.$root.new(Notification, { this.$root.new(Notification, {
message message

View File

@ -148,12 +148,15 @@ export default Vue.extend({
}, },
rename() { rename() {
this.$input({ this.$root.dialog({
title: this.$t('contextmenu.rename-file'), title: this.$t('contextmenu.rename-file'),
placeholder: this.$t('contextmenu.input-new-file-name'), input: {
default: this.file.name, placeholder: this.$t('contextmenu.input-new-file-name'),
allowEmpty: false default: this.file.name,
}).then(name => { allowEmpty: false
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/files/update', { this.$root.api('drive/files/update', {
fileId: this.file.id, fileId: this.file.id,
name: name name: name

View File

@ -192,11 +192,14 @@ export default Vue.extend({
}, },
rename() { rename() {
this.$input({ this.$root.dialog({
title: this.$t('contextmenu.rename-folder'), title: this.$t('contextmenu.rename-folder'),
placeholder: this.$t('contextmenu.input-new-folder-name'), input: {
default: this.folder.name placeholder: this.$t('contextmenu.input-new-folder-name'),
}).then(name => { default: this.folder.name
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/folders/update', { this.$root.api('drive/folders/update', {
folderId: this.folder.id, folderId: this.folder.id,
name: name name: name

View File

@ -331,10 +331,13 @@ export default Vue.extend({
}, },
urlUpload() { urlUpload() {
this.$input({ this.$root.dialog({
title: this.$t('url-upload'), title: this.$t('url-upload'),
placeholder: this.$t('url-of-file') input: {
}).then(url => { placeholder: this.$t('url-of-file')
}
}).then(({ canceled, result: url }) => {
if (canceled) return;
this.$root.api('drive/files/upload_from_url', { this.$root.api('drive/files/upload_from_url', {
url: url, url: url,
folderId: this.folder ? this.folder.id : undefined folderId: this.folder ? this.folder.id : undefined
@ -348,10 +351,13 @@ export default Vue.extend({
}, },
createFolder() { createFolder() {
this.$input({ this.$root.dialog({
title: this.$t('create-folder'), title: this.$t('create-folder'),
placeholder: this.$t('folder-name') input: {
}).then(name => { placeholder: this.$t('folder-name')
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/folders/create', { this.$root.api('drive/folders/create', {
name: name, name: name,
parentId: this.folder ? this.folder.id : undefined parentId: this.folder ? this.folder.id : undefined

View File

@ -1,180 +0,0 @@
<template>
<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="destroyDom">
<span slot="header" :class="$style.header">
<fa icon="i-cursor"/>{{ title }}
</span>
<div :class="$style.body">
<input ref="text" v-model="text" :type="type" @keydown="onKeydown" :placeholder="placeholder"/>
</div>
<div :class="$style.actions">
<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
<button :class="$style.ok" :disabled="!allowEmpty && text.length == 0" @click="ok">{{ $t('ok') }}</button>
</div>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('desktop/views/input-dialog.vue'),
props: {
title: {
type: String
},
placeholder: {
type: String
},
default: {
type: String
},
allowEmpty: {
default: true
},
type: {
default: 'text'
}
},
data() {
return {
done: false,
text: ''
};
},
mounted() {
if (this.default) this.text = this.default;
this.$nextTick(() => {
(this.$refs.text as any).focus();
});
},
methods: {
ok() {
if (!this.allowEmpty && this.text == '') return;
this.done = true;
(this.$refs.window as any).close();
},
cancel() {
this.done = false;
(this.$refs.window as any).close();
},
beforeClose() {
if (this.done) {
this.$emit('done', this.text);
} else {
this.$emit('canceled');
}
},
onKeydown(e) {
if (e.which == 13) { // Enter
e.preventDefault();
e.stopPropagation();
this.ok();
}
}
}
});
</script>
<style lang="stylus" module>
.header
> [data-icon]
margin-right 4px
.body
padding 16px
> input
display block
padding 8px
margin 0
width 100%
max-width 100%
min-width 100%
font-size 1em
color #333
background #fff
outline none
border solid 1px var(--primaryAlpha01)
border-radius 4px
transition border-color .3s ease
&:hover
border-color var(--primaryAlpha02)
transition border-color .1s ease
&:focus
color var(--primary)
border-color var(--primaryAlpha05)
transition border-color 0s ease
&::-webkit-input-placeholder
color var(--primaryAlpha03)
.actions
height 72px
background var(--primaryLighten95)
.ok
.cancel
display block
position absolute
bottom 16px
cursor pointer
padding 0
margin 0
width 120px
height 40px
font-size 1em
outline none
border-radius 4px
&:focus
&:after
content ""
pointer-events none
position absolute
top -5px
right -5px
bottom -5px
left -5px
border 2px solid var(--primaryAlpha03)
border-radius 8px
&:disabled
opacity 0.7
cursor default
.ok
right 16px
color var(--primaryForeground)
background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
border solid 1px var(--primaryLighten15)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
border-color var(--primary)
&:active:not(:disabled)
background var(--primary)
border-color var(--primary)
.cancel
right 148px
color #888
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
border solid 1px #e2e2e2
&:hover
background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
border-color #dcdcdc
&:active
background #ececec
border-color #dcdcdc
</style>

View File

@ -384,13 +384,12 @@ export default Vue.extend({
}, },
addVisibleUser() { addVisibleUser() {
this.$input({ this.$root.dialog({
title: this.$t('enter-username') title: this.$t('enter-username'),
}).then(acct => { user: true
if (acct.startsWith('@')) acct = acct.substr(1); }).then(({ canceled, result: user }) => {
this.$root.api('users/show', parseAcct(acct)).then(user => { if (canceled) return;
this.visibleUsers.push(user); this.visibleUsers.push(user);
});
}); });
}, },

View File

@ -35,10 +35,13 @@ export default Vue.extend({
}, },
methods: { methods: {
register() { register() {
this.$input({ this.$root.dialog({
title: this.$t('enter-password'), title: this.$t('enter-password'),
type: 'password' input: {
}).then(password => { type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
this.$root.api('i/2fa/register', { this.$root.api('i/2fa/register', {
password: password password: password
}).then(data => { }).then(data => {
@ -48,10 +51,13 @@ export default Vue.extend({
}, },
unregister() { unregister() {
this.$input({ this.$root.dialog({
title: this.$t('enter-password'), title: this.$t('enter-password'),
type: 'password' input: {
}).then(password => { type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
this.$root.api('i/2fa/unregister', { this.$root.api('i/2fa/unregister', {
password: password password: password
}).then(() => { }).then(() => {

View File

@ -109,9 +109,11 @@ export default Vue.extend({
icon: 'plus', icon: 'plus',
text: this.$t('add-list'), text: this.$t('add-list'),
action: () => { action: () => {
this.$input({ this.$root.dialog({
title: this.$t('list-name'), title: this.$t('list-name'),
}).then(async title => { input: true
}).then(async ({ canceled, result: title }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', { const list = await this.$root.api('users/lists/create', {
title title
}); });

View File

@ -29,9 +29,11 @@ export default Vue.extend({
}, },
methods: { methods: {
add() { add() {
this.$input({ this.$root.dialog({
title: this.$t('list-name'), title: this.$t('list-name'),
}).then(async title => { input: true
}).then(async ({ canceled, result: title }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', { const list = await this.$root.api('users/lists/create', {
title title
}); });

View File

@ -167,11 +167,14 @@ export default Vue.extend({
icon: 'pencil-alt', icon: 'pencil-alt',
text: this.$t('rename'), text: this.$t('rename'),
action: () => { action: () => {
this.$input({ this.$root.dialog({
title: this.$t('rename'), title: this.$t('rename'),
default: this.name, input: {
allowEmpty: false default: this.name,
}).then(name => { allowEmpty: false
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$store.dispatch('settings/renameDeckColumn', { id: this.column.id, name }); this.$store.dispatch('settings/renameDeckColumn', { id: this.column.id, name });
}); });
} }

View File

@ -252,9 +252,11 @@ export default Vue.extend({
icon: 'hashtag', icon: 'hashtag',
text: this.$t('@deck.hashtag'), text: this.$t('@deck.hashtag'),
action: () => { action: () => {
this.$input({ this.$root.dialog({
title: this.$t('enter-hashtag-tl-title') title: this.$t('enter-hashtag-tl-title'),
}).then(title => { input: true
}).then(({ canceled, result: title }) => {
if (canceled) return;
this.$store.dispatch('settings/addDeckColumn', { this.$store.dispatch('settings/addDeckColumn', {
id: uuid(), id: uuid(),
type: 'hashtag', type: 'hashtag',

View File

@ -77,8 +77,8 @@ export default Vue.extend({
type: 'warning', type: 'warning',
text: this.$t('block-confirm'), text: this.$t('block-confirm'),
showCancelButton: true showCancelButton: true
}).then(res => { }).then(({ canceled }) => {
if (!res) return; if (canceled) return;
this.$root.api('blocking/create', { this.$root.api('blocking/create', {
userId: this.user.id userId: this.user.id

View File

@ -460,8 +460,8 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS]) => void,
dialog(opts) { dialog(opts) {
return new Promise((res) => { return new Promise((res) => {
const vm = this.new(Dialog, opts); const vm = this.new(Dialog, opts);
vm.$once('ok', result => res(result)); vm.$once('ok', result => res({ canceled: false, result }));
vm.$once('cancel', () => res(false)); vm.$once('cancel', () => res({ canceled: true }));
}); });
} }
}, },

View File

@ -95,15 +95,6 @@ init((launch) => {
}); });
}, },
$input(opts) {
return new Promise<string>((res, rej) => {
const x = window.prompt(opts.title);
if (x) {
res(x);
}
});
},
$notify(message) { $notify(message) {
alert(message); alert(message);
} }

View File

@ -27,8 +27,8 @@ export default Vue.extend({
type: 'warning', type: 'warning',
text: this.$t('read-all'), text: this.$t('read-all'),
showCancelButton: true showCancelButton: true
}).then(res => { }).then(({ canceled }) => {
if (!res) return; if (canceled) return;
this.$root.api('notifications/mark_all_as_read'); this.$root.api('notifications/mark_all_as_read');
}); });

View File

@ -38,9 +38,11 @@ export default Vue.extend({
}, },
methods: { methods: {
fn() { fn() {
this.$input({ this.$root.dialog({
title: this.$t('enter-list-name'), title: this.$t('enter-list-name'),
}).then(async title => { input: true
}).then(async ({ canceled, result: title }) => {
if (canceled) return;
const list = await this.$root.api('users/lists/create', { const list = await this.$root.api('users/lists/create', {
title title
}); });

View File

@ -120,7 +120,7 @@ export default Vue.extend({
text: this.$t('push-to-list'), text: this.$t('push-to-list'),
action: async () => { action: async () => {
const lists = await this.$root.api('users/lists/list'); const lists = await this.$root.api('users/lists/list');
const listId = await this.$root.dialog({ const { canceled, result: listId } = await this.$root.dialog({
type: null, type: null,
title: this.$t('select-list'), title: this.$t('select-list'),
select: { select: {
@ -130,7 +130,7 @@ export default Vue.extend({
}, },
showCancelButton: true showCancelButton: true
}); });
if (!listId) return; if (canceled) return;
await this.$root.api('users/lists/push', { await this.$root.api('users/lists/push', {
listId: listId, listId: listId,
userId: this.user.id userId: this.user.id