From cd79314f69eb7a082327828538413e46403e8303 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Mon, 7 Sep 2020 14:19:53 +0900 Subject: [PATCH] wip --- src/client/components/autocomplete.vue | 2 +- .../components/deck/notifications-column.vue | 2 +- src/client/components/dialog.vue | 89 ++++++++---------- src/client/components/media-image.vue | 2 +- src/client/components/menu.vue | 70 ++++++-------- src/client/components/modal.vue | 89 +++++++++--------- src/client/components/note.vue | 16 ++-- src/client/components/notes.vue | 7 +- src/client/components/particle.vue | 5 +- src/client/components/post-form-dialog.vue | 94 ------------------- src/client/components/post-form.vue | 20 ++-- src/client/components/reaction-picker.vue | 22 +---- .../components/reactions-viewer.reaction.vue | 2 +- src/client/components/sidebar.vue | 4 +- src/client/deck.vue | 2 +- src/client/default.vue | 2 +- src/client/os.ts | 67 +++++++++---- src/client/pages/index.welcome.entrance.vue | 4 +- src/client/pages/instance/federation.vue | 2 +- src/client/pages/instance/index.vue | 2 +- src/client/pages/instance/instance.vue | 6 +- src/client/pages/instance/settings.vue | 4 +- src/client/pages/instance/users.vue | 2 +- src/client/pages/messaging/index.vue | 2 +- .../pages/messaging/messaging-room.form.vue | 2 +- .../pages/my-antennas/index.antenna.vue | 2 +- src/client/pages/my-groups/group.vue | 4 +- src/client/pages/my-lists/list.vue | 2 +- src/client/pages/my-settings/api.vue | 2 +- src/client/pages/my-settings/index.vue | 2 +- src/client/pages/my-settings/reaction.vue | 4 +- src/client/pages/preferences/plugins.vue | 2 +- src/client/pages/share.vue | 2 +- src/client/pages/user/index.vue | 2 +- src/client/root.vue | 9 +- src/client/store.ts | 1 + src/client/widgets/notifications.vue | 2 +- src/client/widgets/welcome.vue | 4 +- 38 files changed, 240 insertions(+), 317 deletions(-) delete mode 100644 src/client/components/post-form-dialog.vue diff --git a/src/client/components/autocomplete.vue b/src/client/components/autocomplete.vue index 2ec6e8a3b9..1faa5aeb7d 100644 --- a/src/client/components/autocomplete.vue +++ b/src/client/components/autocomplete.vue @@ -376,7 +376,7 @@ export default defineComponent({ chooseUser() { this.close(); - const vm = os.popup(MkUserSelect, {}); + const vm = os.modal(MkUserSelect, {}); vm.$once('selected', user => { this.complete('user', user); }); diff --git a/src/client/components/deck/notifications-column.vue b/src/client/components/deck/notifications-column.vue index a8ffbaa575..94efe8a03f 100644 --- a/src/client/components/deck/notifications-column.vue +++ b/src/client/components/deck/notifications-column.vue @@ -43,7 +43,7 @@ export default defineComponent({ icon: faCog, text: this.$t('notificationSetting'), action: async () => { - os.popup(await import('../notification-setting-window.vue'), { + os.modal(await import('../notification-setting-window.vue'), { includingTypes: this.column.includingTypes, }).$on('ok', async ({ includingTypes }) => { this.$set(this.column, 'includingTypes', includingTypes); diff --git a/src/client/components/dialog.vue b/src/client/components/dialog.vue index 8a2edf9da4..141d2ab7f7 100644 --- a/src/client/components/dialog.vue +++ b/src/client/components/dialog.vue @@ -1,46 +1,44 @@ <template> -<x-modal @closed="$emit('closed')" @click="onBgClick" :showing="showing"> - <div class="mk-dialog" :class="{ iconOnly }"> - <template v-if="type == 'signin'"> - <mk-signin/> - </template> - <template v-else> - <div class="icon" v-if="icon"> - <fa :icon="icon"/> - </div> - <div class="icon" v-else-if="!input && !select && !user" :class="type"> - <fa :icon="faCheck" v-if="type === 'success'"/> - <fa :icon="faTimesCircle" v-if="type === 'error'"/> - <fa :icon="faExclamationTriangle" v-if="type === 'warning'"/> - <fa :icon="faInfoCircle" v-if="type === 'info'"/> - <fa :icon="faQuestionCircle" v-if="type === 'question'"/> - <fa :icon="faSpinner" pulse v-if="type === 'waiting'"/> - </div> - <header v-if="title" v-html="title"></header> - <header v-if="title == null && user">{{ $t('enterUsername') }}</header> - <div class="body" v-if="text" v-html="text"></div> - <mk-input v-if="input" v-model:value="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></mk-input> - <mk-input v-if="user" v-model:value="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></mk-input> - <mk-select v-if="select" v-model:value="selectedValue" autofocus> - <template v-if="select.items"> - <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> - </template> - <template v-else> - <optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label"> - <option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option> - </optgroup> - </template> - </mk-select> - <div class="buttons" v-if="!iconOnly && (showOkButton || showCancelButton) && !actions"> - <mk-button inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</mk-button> - <mk-button inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</mk-button> - </div> - <div class="buttons" v-if="actions"> - <mk-button v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</mk-button> - </div> - </template> - </div> -</x-modal> +<div class="mk-dialog" :class="{ iconOnly }"> + <template v-if="type == 'signin'"> + <mk-signin/> + </template> + <template v-else> + <div class="icon" v-if="icon"> + <fa :icon="icon"/> + </div> + <div class="icon" v-else-if="!input && !select && !user" :class="type"> + <fa :icon="faCheck" v-if="type === 'success'"/> + <fa :icon="faTimesCircle" v-if="type === 'error'"/> + <fa :icon="faExclamationTriangle" v-if="type === 'warning'"/> + <fa :icon="faInfoCircle" v-if="type === 'info'"/> + <fa :icon="faQuestionCircle" v-if="type === 'question'"/> + <fa :icon="faSpinner" pulse v-if="type === 'waiting'"/> + </div> + <header v-if="title" v-html="title"></header> + <header v-if="title == null && user">{{ $t('enterUsername') }}</header> + <div class="body" v-if="text" v-html="text"></div> + <mk-input v-if="input" v-model:value="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></mk-input> + <mk-input v-if="user" v-model:value="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></mk-input> + <mk-select v-if="select" v-model:value="selectedValue" autofocus> + <template v-if="select.items"> + <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> + </template> + <template v-else> + <optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label"> + <option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option> + </optgroup> + </template> + </mk-select> + <div class="buttons" v-if="!iconOnly && (showOkButton || showCancelButton) && !actions"> + <mk-button inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</mk-button> + <mk-button inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</mk-button> + </div> + <div class="buttons" v-if="actions"> + <mk-button v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</mk-button> + </div> + </template> +</div> </template> <script lang="ts"> @@ -52,12 +50,10 @@ import MkInput from './ui/input.vue'; import MkSelect from './ui/select.vue'; import MkSignin from './signin.vue'; import parseAcct from '../../misc/acct/parse'; -import XModal from './modal.vue'; import * as os from '@/os'; export default defineComponent({ components: { - XModal, MkButton, MkInput, MkSelect, @@ -65,9 +61,6 @@ export default defineComponent({ }, props: { - showing: { - required: true - }, type: { type: String, required: false, @@ -118,7 +111,7 @@ export default defineComponent({ }, }, - emits: ['done', 'closed'], + emits: ['done'], data() { return { diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue index 3facebd970..25469fb831 100644 --- a/src/client/components/media-image.vue +++ b/src/client/components/media-image.vue @@ -72,7 +72,7 @@ export default defineComponent({ if (this.$store.state.device.imageNewTab) { window.open(this.image.url, '_blank'); } else { - const viewer = os.popup(ImageViewer, { + const viewer = os.modal(ImageViewer, { image: this.image }); this.$once('hook:beforeDestroy', () => { diff --git a/src/client/components/menu.vue b/src/client/components/menu.vue index 820e4c3dde..5b5a64335e 100644 --- a/src/client/components/menu.vue +++ b/src/client/components/menu.vue @@ -1,55 +1,43 @@ <template> -<x-modal :source="source" :no-center="noCenter" @click="close()" @closed="$emit('closed')" :showing="showing"> - <div class="rrevdjwt" :class="{ left: align === 'left' }" ref="items" :style="{ width: width + 'px' }"> - <template v-for="(item, i) in items.filter(item => item !== undefined)"> - <div v-if="item === null" class="divider" :key="i"></div> - <span v-else-if="item.type === 'label'" class="label item" :key="i"> - <span>{{ item.text }}</span> - </span> - <router-link v-else-if="item.type === 'link'" :to="item.to" @click.native="close()" :tabindex="i" class="_button item" :key="i"> - <fa v-if="item.icon" :icon="item.icon" fixed-width/> - <mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/> - <span>{{ item.text }}</span> - <i v-if="item.indicate"><fa :icon="faCircle"/></i> - </router-link> - <a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item" :key="i"> - <fa v-if="item.icon" :icon="item.icon" fixed-width/> - <span>{{ item.text }}</span> - <i v-if="item.indicate"><fa :icon="faCircle"/></i> - </a> - <button v-else-if="item.type === 'user'" @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i"> - <mk-avatar :user="item.user" class="avatar"/><mk-user-name :user="item.user"/> - <i v-if="item.indicate"><fa :icon="faCircle"/></i> - </button> - <button v-else @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i"> - <fa v-if="item.icon" :icon="item.icon" fixed-width/> - <mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/> - <span>{{ item.text }}</span> - <i v-if="item.indicate"><fa :icon="faCircle"/></i> - </button> - </template> - </div> -</x-modal> +<div class="rrevdjwt" :class="{ left: align === 'left' }" ref="items" :style="{ width: width + 'px' }"> + <template v-for="(item, i) in items.filter(item => item !== undefined)"> + <div v-if="item === null" class="divider" :key="i"></div> + <span v-else-if="item.type === 'label'" class="label item" :key="i"> + <span>{{ item.text }}</span> + </span> + <router-link v-else-if="item.type === 'link'" :to="item.to" @click.native="close()" :tabindex="i" class="_button item" :key="i"> + <fa v-if="item.icon" :icon="item.icon" fixed-width/> + <mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/> + <span>{{ item.text }}</span> + <i v-if="item.indicate"><fa :icon="faCircle"/></i> + </router-link> + <a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item" :key="i"> + <fa v-if="item.icon" :icon="item.icon" fixed-width/> + <span>{{ item.text }}</span> + <i v-if="item.indicate"><fa :icon="faCircle"/></i> + </a> + <button v-else-if="item.type === 'user'" @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i"> + <mk-avatar :user="item.user" class="avatar"/><mk-user-name :user="item.user"/> + <i v-if="item.indicate"><fa :icon="faCircle"/></i> + </button> + <button v-else @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i"> + <fa v-if="item.icon" :icon="item.icon" fixed-width/> + <mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/> + <span>{{ item.text }}</span> + <i v-if="item.indicate"><fa :icon="faCircle"/></i> + </button> + </template> +</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { faCircle } from '@fortawesome/free-solid-svg-icons'; -import XModal from './modal.vue'; import { focusPrev, focusNext } from '@/scripts/focus'; import * as os from '@/os'; export default defineComponent({ - components: { - XModal - }, props: { - showing: { - required: true - }, - source: { - required: true - }, items: { type: Array, required: true diff --git a/src/client/components/modal.vue b/src/client/components/modal.vue index 62ffc3f69c..7c016edc9e 100644 --- a/src/client/components/modal.vue +++ b/src/client/components/modal.vue @@ -18,6 +18,9 @@ import * as os from '@/os'; // memo: 旧popup.vueのfixedプロパティに相当するものはsource要素の祖先を辿るなどして自動で判定できるのでは export default defineComponent({ + provide: { + modal: true + }, props: { showing: { type: Boolean, @@ -59,58 +62,60 @@ export default defineComponent({ const popover = this.$refs.content as any; - const rect = this.source.getBoundingClientRect(); - const width = popover.offsetWidth; - const height = popover.offsetHeight; + new ResizeObserver((entries, observer) => { + const rect = this.source.getBoundingClientRect(); + const width = popover.offsetWidth; + const height = popover.offsetHeight; - let left; - let top; + let left; + let top; - if (os.isMobile && !this.noCenter) { - const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2); - const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.source.offsetHeight / 2); - left = (x - (width / 2)); - top = (y - (height / 2)); - popover.style.transformOrigin = 'center'; - } else { - const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2); - const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.source.offsetHeight; - left = (x - (width / 2)); - top = y; - } - - if (this.fixed) { - if (left + width > window.innerWidth) { - left = window.innerWidth - width; + if (os.isMobile && !this.noCenter) { + const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2); + const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.source.offsetHeight / 2); + left = (x - (width / 2)); + top = (y - (height / 2)); popover.style.transformOrigin = 'center'; + } else { + const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2); + const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.source.offsetHeight; + left = (x - (width / 2)); + top = y; } - if (top + height > window.innerHeight) { - top = window.innerHeight - height; - popover.style.transformOrigin = 'center'; - } - } else { - if (left + width - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - width + window.pageXOffset; - popover.style.transformOrigin = 'center'; + if (this.fixed) { + if (left + width > window.innerWidth) { + left = window.innerWidth - width; + popover.style.transformOrigin = 'center'; + } + + if (top + height > window.innerHeight) { + top = window.innerHeight - height; + popover.style.transformOrigin = 'center'; + } + } else { + if (left + width - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - width + window.pageXOffset; + popover.style.transformOrigin = 'center'; + } + + if (top + height - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - height + window.pageYOffset; + popover.style.transformOrigin = 'center'; + } } - if (top + height - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - height + window.pageYOffset; - popover.style.transformOrigin = 'center'; + if (top < 0) { + top = 0; } - } - if (top < 0) { - top = 0; - } + if (left < 0) { + left = 0; + } - if (left < 0) { - left = 0; - } - - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; + popover.style.left = left + 'px'; + popover.style.top = top + 'px'; + }).observe(popover); }); }, }); diff --git a/src/client/components/note.vue b/src/client/components/note.vue index e74ec55d06..926f8e0ccf 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -96,7 +96,7 @@ </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { defineAsyncComponent, defineComponent } from 'vue'; import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons'; import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; import { parse } from '../../mfm/parse'; @@ -108,8 +108,6 @@ import XReactionsViewer from './reactions-viewer.vue'; import XMediaList from './media-list.vue'; import XCwButton from './cw-button.vue'; import XPoll from './poll.vue'; -import MkUrlPreview from './url-preview.vue'; -import MkReactionPicker from './reaction-picker.vue'; import { pleaseLogin } from '@/scripts/please-login'; import { focusPrev, focusNext } from '@/scripts/focus'; import { url } from '@/config'; @@ -133,7 +131,7 @@ export default defineComponent({ XMediaList, XCwButton, XPoll, - MkUrlPreview, + MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), }, inject: { @@ -485,17 +483,17 @@ export default defineComponent({ react(viaKeyboard = false) { pleaseLogin(); this.blur(); - const close = os.popup(MkReactionPicker, { - source: this.$refs.reactButton, + os.modal(defineAsyncComponent(() => import('@/components/reaction-picker.vue')), { showFocus: viaKeyboard, }, reaction => { os.api('notes/reactions/create', { noteId: this.appearNote.id, reaction: reaction - }).then(() => { - close(); }); - }, this.focus); + this.focus(); + }, { + source: this.$refs.reactButton + }); }, reactDirectly(reaction) { diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 22fc9e64f9..14eb26ba91 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -32,12 +32,11 @@ import { defineComponent } from 'vue'; import paging from '@/scripts/paging'; import XNote from './note.vue'; import XList from './date-separated-list.vue'; -import MkButton from './ui/button.vue'; import * as os from '@/os'; export default defineComponent({ components: { - XNote, XList, MkButton + XNote, XList, }, mixins: [ @@ -83,9 +82,9 @@ export default defineComponent({ updated(oldValue, newValue) { const i = this.notes.findIndex(n => n === oldValue); if (this.prop) { - Vue.set(this.items[i], this.prop, newValue); + this.items[i][this.prop] = newValue; } else { - Vue.set(this.items, i, newValue); + this.items[i] = newValue; } }, diff --git a/src/client/components/particle.vue b/src/client/components/particle.vue index 3fe0256cf6..c392f3066f 100644 --- a/src/client/components/particle.vue +++ b/src/client/components/particle.vue @@ -57,9 +57,6 @@ import * as os from '@/os'; export default defineComponent({ props: { - destroy: { - required: true - }, x: { type: Number, required: true @@ -94,7 +91,7 @@ export default defineComponent({ }, mounted() { setTimeout(() => { - this.destroy(); + this.$emit('closed'); }, 1100); } }); diff --git a/src/client/components/post-form-dialog.vue b/src/client/components/post-form-dialog.vue deleted file mode 100644 index c0231d0e3f..0000000000 --- a/src/client/components/post-form-dialog.vue +++ /dev/null @@ -1,94 +0,0 @@ -<template> -<x-modal @closed="$emit('closed')" @click="onBgClick" :showing="showing"> - <x-post-form ref="form" class="ulveipgl" - :reply="reply" - :renote="renote" - :mention="mention" - :specified="specified" - :initial-text="initialText" - :initial-note="initialNote" - :instant="instant" - @posted="onPosted" - @cancel="onCanceled" - /> -</x-modal> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XModal from './modal.vue'; -import XPostForm from './post-form.vue'; -import * as os from '@/os'; - -export default defineComponent({ - components: { - XModal, - XPostForm, - }, - - props: { - showing: { - required: true - }, - reply: { - type: Object, - required: false - }, - renote: { - type: Object, - required: false - }, - mention: { - type: Object, - required: false - }, - specified: { - type: Object, - required: false - }, - initialText: { - type: String, - required: false - }, - initialNote: { - type: Object, - required: false - }, - instant: { - type: Boolean, - required: false, - default: false - } - }, - - methods: { - focus() { - this.$refs.form.focus(); - }, - - onPosted() { - this.$emit('done', 'posted'); - }, - - onCanceled() { - this.$emit('done', 'canceled'); - }, - - onKeydown(e) { - if (e.which === 27) { // Esc - e.preventDefault(); - e.stopPropagation(); - this.$emit('done', 'canceled'); - } - }, - } -}); -</script> - -<style lang="scss" scoped> -.ulveipgl { - width: 100%; - max-width: 500px; - border-radius: var(--radius); -} -</style> diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index 410ebb90cf..35739082f0 100644 --- a/src/client/components/post-form.vue +++ b/src/client/components/post-form.vue @@ -1,5 +1,5 @@ <template> -<div class="gafaadew" +<div class="gafaadew" :class="{ modal }" @dragover.stop="onDragover" @dragenter="onDragenter" @dragleave="onDragleave" @@ -80,6 +80,8 @@ export default defineComponent({ XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')) }, + inject: ['modal'], + props: { reply: { type: Object, @@ -417,7 +419,7 @@ export default defineComponent({ // TODO: information dialog return; } - const w = os.popup(MkVisibilityChooser, { + const w = os.modal(MkVisibilityChooser, { source: this.$refs.visibilityButton, currentVisibility: this.visibility, currentLocalOnly: this.localOnly @@ -433,7 +435,7 @@ export default defineComponent({ }, addVisibleUser() { - const vm = os.popup(MkUserSelect, {}); + const vm = os.modal(MkUserSelect, {}); vm.$once('selected', user => { this.visibleUsers.push(user); }); @@ -593,18 +595,18 @@ export default defineComponent({ }, cancel() { - this.$emit('cancel'); + this.$emit('done'); }, insertMention() { - const vm = os.popup(MkUserSelect, {}); + const vm = os.modal(MkUserSelect, {}); vm.$once('selected', user => { insertTextAtCursor(this.$refs.text, getAcct(user) + ' '); }); }, async insertEmoji(ev) { - const vm = os.popup(await import('./emoji-picker.vue'), { + const vm = os.modal(await import('./emoji-picker.vue'), { source: ev.currentTarget || ev.target }).$once('chosen', emoji => { insertTextAtCursor(this.$refs.text, emoji); @@ -636,6 +638,12 @@ export default defineComponent({ position: relative; background: var(--panel); + &.modal { + width: 100%; + max-width: 500px; + border-radius: var(--radius); + } + > header { z-index: 1000; height: 66px; diff --git a/src/client/components/reaction-picker.vue b/src/client/components/reaction-picker.vue index e6e9d4137d..96549fd67a 100644 --- a/src/client/components/reaction-picker.vue +++ b/src/client/components/reaction-picker.vue @@ -1,36 +1,24 @@ <template> -<XModal :source="source" @closed="$emit('closed')" :showing="showing" @click="close" v-hotkey.global="keymap"> - <div class="rdfaahpb"> - <div class="buttons" ref="buttons" :class="{ showFocus }"> - <button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="reaction" v-particle><x-reaction-icon :reaction="reaction"/></button> - </div> - <input class="text" v-model.trim="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }"> +<div class="rdfaahpb"> + <div class="buttons" ref="buttons" :class="{ showFocus }"> + <button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="reaction" v-particle><x-reaction-icon :reaction="reaction"/></button> </div> -</XModal> + <input class="text" v-model.trim="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }"> +</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { emojiRegex } from '../../misc/emoji-regex'; import XReactionIcon from './reaction-icon.vue'; -import XModal from './modal.vue'; import * as os from '@/os'; export default defineComponent({ components: { - XModal, XReactionIcon, }, props: { - showing: { - required: true - }, - - source: { - required: true - }, - reactions: { required: false }, diff --git a/src/client/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue index f61374b7e2..60cc4bd2e4 100644 --- a/src/client/components/reactions-viewer.reaction.vue +++ b/src/client/components/reactions-viewer.reaction.vue @@ -111,7 +111,7 @@ export default defineComponent({ this.closeDetails(); if (!this.isHovering) return; - this.details = os.popup(XDetails, { + this.details = os.modal(XDetails, { reaction: this.reaction, users, count: this.count, diff --git a/src/client/components/sidebar.vue b/src/client/components/sidebar.vue index 1f60a1bdb1..ab8f62916e 100644 --- a/src/client/components/sidebar.vue +++ b/src/client/components/sidebar.vue @@ -276,7 +276,7 @@ export default defineComponent({ }, async addAcount() { - os.popup(await import('./signin-dialog.vue')).$once('login', res => { + os.modal(await import('./signin-dialog.vue')).$once('login', res => { this.$store.dispatch('addAcount', res); os.dialog({ type: 'success', @@ -286,7 +286,7 @@ export default defineComponent({ }, async createAccount() { - os.popup(await import('./signup-dialog.vue')).$once('signup', res => { + os.modal(await import('./signup-dialog.vue')).$once('signup', res => { this.$store.dispatch('addAcount', res); this.switchAccountWithToken(res.i); }); diff --git a/src/client/deck.vue b/src/client/deck.vue index 15883fb933..0f6a0f4255 100644 --- a/src/client/deck.vue +++ b/src/client/deck.vue @@ -166,7 +166,7 @@ export default defineComponent({ id: notification.id }); - os.popup(await import('@/components/toast.vue'), { + os.modal(await import('@/components/toast.vue'), { notification }); } diff --git a/src/client/default.vue b/src/client/default.vue index 2899f684ee..cf92d226db 100644 --- a/src/client/default.vue +++ b/src/client/default.vue @@ -335,7 +335,7 @@ export default defineComponent({ id: notification.id }); - os.popup(await import('@/components/toast.vue'), { + os.modal(await import('@/components/toast.vue'), { notification }); } diff --git a/src/client/os.ts b/src/client/os.ts index e6e6c2a76f..f9ef20c85d 100644 --- a/src/client/os.ts +++ b/src/client/os.ts @@ -1,4 +1,4 @@ -import { Component, defineAsyncComponent, ref } from 'vue'; +import { Component, defineAsyncComponent, markRaw, ref } from 'vue'; import Stream from '@/scripts/stream'; import { store } from '@/store'; import { apiUrl } from '@/config'; @@ -44,46 +44,81 @@ export function api(endpoint: string, data: Record<string, any> = {}, token?: st return promise; } -export function popup(component: Component, props: Record<string, any>, callback?: Function) { +export function popup(component: Component, props: Record<string, any>, callback?: Function, option?) { + markRaw(component); const id = Math.random().toString(); // TODO: uuidとか使う const showing = ref(true); - const popup = { + const close = (...args) => { + if (callback) callback(...args); + showing.value = false; + }; + const modal = { + type: 'popup', component, - props: { - ...props, - showing - }, + props, showing, - done: (...args) => { - if (callback) callback(...args); - showing.value = false; - }, + source: option?.source, + done: close, + bgClick: () => close(), closed: () => { store.commit('removePopup', id); }, id, }; - store.commit('addPopup', popup); - return () => { + store.commit('addPopup', modal); + return close; +} + +export function modal(component: Component, props: Record<string, any>, callback?: Function, option?) { + markRaw(component); + const id = Math.random().toString(); // TODO: uuidとか使う + const showing = ref(true); + const close = (...args) => { + if (callback) callback(...args); showing.value = false; }; + const modal = { + type: 'modal', + component, + props, + showing, + source: option?.source, + done: close, + bgClick: () => close(), + closed: () => { + store.commit('removePopup', id); + }, + id, + }; + store.commit('addPopup', modal); + return close; } export function dialog(props: Record<string, any>) { return new Promise((res, rej) => { - popup(defineAsyncComponent(() => import('@/components/dialog.vue')), props, res); + modal(defineAsyncComponent(() => import('@/components/dialog.vue')), props, result => { + if (result) { + res(result); + } else { + res({ canceled: true }); + } + }); }); } export function menu(props: Record<string, any>) { + const source = props.source; // TODO: sourceはpropsの外に出して追加の引数として受け取るようにする return new Promise((res, rej) => { - popup(defineAsyncComponent(() => import('@/components/menu.vue')), props, res); + modal(defineAsyncComponent(() => import('@/components/menu.vue')), props, res, { + position: 'source', + source + }); }); } export function post(props: Record<string, any>) { return new Promise((res, rej) => { - popup(defineAsyncComponent(() => import('@/components/post-form-dialog.vue')), props, res); + modal(defineAsyncComponent(() => import('@/components/post-form.vue')), props, res); }); } diff --git a/src/client/pages/index.welcome.entrance.vue b/src/client/pages/index.welcome.entrance.vue index d1fab0b236..0715d0c062 100644 --- a/src/client/pages/index.welcome.entrance.vue +++ b/src/client/pages/index.welcome.entrance.vue @@ -54,13 +54,13 @@ export default defineComponent({ methods: { signin() { - os.popup(XSigninDialog, { + os.modal(XSigninDialog, { autoSet: true }); }, signup() { - os.popup(XSignupDialog, { + os.modal(XSignupDialog, { autoSet: true }); } diff --git a/src/client/pages/instance/federation.vue b/src/client/pages/instance/federation.vue index 81860eee12..0044bbd8f0 100644 --- a/src/client/pages/instance/federation.vue +++ b/src/client/pages/instance/federation.vue @@ -125,7 +125,7 @@ export default defineComponent({ }, info(instance) { - os.popup(MkInstanceInfo, { + os.modal(MkInstanceInfo, { instance: instance }); } diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index 668a4a2e1f..64044cc6b6 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -550,7 +550,7 @@ export default defineComponent({ host: q }); } - os.popup(MkInstanceInfo, { + os.modal(MkInstanceInfo, { instance: instance }); }, diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/instance/instance.vue index 88cbce5514..95c49b9791 100644 --- a/src/client/pages/instance/instance.vue +++ b/src/client/pages/instance/instance.vue @@ -439,7 +439,7 @@ export default defineComponent({ }, showFollowing() { - os.popup(MkUsersDialog, { + os.modal(MkUsersDialog, { title: this.$t('instanceFollowing'), pagination: { endpoint: 'federation/following', @@ -453,7 +453,7 @@ export default defineComponent({ }, showFollowers() { - os.popup(MkUsersDialog, { + os.modal(MkUsersDialog, { title: this.$t('instanceFollowers'), pagination: { endpoint: 'federation/followers', @@ -467,7 +467,7 @@ export default defineComponent({ }, showUsers() { - os.popup(MkUsersDialog, { + os.modal(MkUsersDialog, { title: this.$t('instanceUsers'), pagination: { endpoint: 'federation/users', diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index 294ec4fb03..5dfbd21f4d 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -452,7 +452,7 @@ export default defineComponent({ }, addPinUser() { - os.popup(MkUserSelect, {}).$once('selected', user => { + os.modal(MkUserSelect, {}).$once('selected', user => { this.pinnedUsers = this.pinnedUsers.trim(); this.pinnedUsers += '\n@' + getAcct(user); this.pinnedUsers = this.pinnedUsers.trim(); @@ -460,7 +460,7 @@ export default defineComponent({ }, chooseProxyAccount() { - os.popup(MkUserSelect, {}).$once('selected', user => { + os.modal(MkUserSelect, {}).$once('selected', user => { this.proxyAccount = user; this.proxyAccountId = user.id; this.save(true); diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue index 56bfa353fe..919627c0a3 100644 --- a/src/client/pages/instance/users.vue +++ b/src/client/pages/instance/users.vue @@ -180,7 +180,7 @@ export default defineComponent({ }, searchUser() { - os.popup(MkUserSelect, {}).$once('selected', user => { + os.modal(MkUserSelect, {}).$once('selected', user => { this.show(user); }); }, diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue index f5ab27ce02..618f49182d 100644 --- a/src/client/pages/messaging/index.vue +++ b/src/client/pages/messaging/index.vue @@ -132,7 +132,7 @@ export default defineComponent({ }, async startUser() { - os.popup(MkUserSelect, {}).$once('selected', user => { + os.modal(MkUserSelect, {}).$once('selected', user => { this.$router.push(`/my/messaging/${getAcct(user)}`); }); }, diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue index 07d21bbf14..81229b7d70 100644 --- a/src/client/pages/messaging/messaging-room.form.vue +++ b/src/client/pages/messaging/messaging-room.form.vue @@ -220,7 +220,7 @@ export default defineComponent({ }, async insertEmoji(ev) { - const vm = os.popup(await import('@/components/emoji-picker.vue'), { + const vm = os.modal(await import('@/components/emoji-picker.vue'), { source: ev.currentTarget || ev.target }).$once('chosen', emoji => { insertTextAtCursor(this.$refs.text, emoji); diff --git a/src/client/pages/my-antennas/index.antenna.vue b/src/client/pages/my-antennas/index.antenna.vue index 67f69a1250..52af31606d 100644 --- a/src/client/pages/my-antennas/index.antenna.vue +++ b/src/client/pages/my-antennas/index.antenna.vue @@ -177,7 +177,7 @@ export default defineComponent({ }, addUser() { - os.popup(MkUserSelect, {}).$once('selected', user => { + os.modal(MkUserSelect, {}).$once('selected', user => { this.users = this.users.trim(); this.users += '\n@' + getAcct(user); this.users = this.users.trim(); diff --git a/src/client/pages/my-groups/group.vue b/src/client/pages/my-groups/group.vue index 580aa345cf..e0edffa80d 100644 --- a/src/client/pages/my-groups/group.vue +++ b/src/client/pages/my-groups/group.vue @@ -89,7 +89,7 @@ export default defineComponent({ }, invite() { - os.popup(MkUserSelect, {}).$once('selected', user => { + os.modal(MkUserSelect, {}).$once('selected', user => { os.api('users/groups/invite', { groupId: this.group.id, userId: user.id @@ -134,7 +134,7 @@ export default defineComponent({ }, transfer() { - os.popup(MkUserSelect, {}).$once('selected', user => { + os.modal(MkUserSelect, {}).$once('selected', user => { os.api('users/groups/transfer', { groupId: this.group.id, userId: user.id diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue index 961cf02404..9185323c98 100644 --- a/src/client/pages/my-lists/list.vue +++ b/src/client/pages/my-lists/list.vue @@ -88,7 +88,7 @@ export default defineComponent({ }, addUser() { - os.popup(MkUserSelect, {}).$once('selected', user => { + os.modal(MkUserSelect, {}).$once('selected', user => { os.api('users/lists/push', { listId: this.list.id, userId: user.id diff --git a/src/client/pages/my-settings/api.vue b/src/client/pages/my-settings/api.vue index a6a8fc5017..a163c9a029 100644 --- a/src/client/pages/my-settings/api.vue +++ b/src/client/pages/my-settings/api.vue @@ -26,7 +26,7 @@ export default defineComponent({ }, methods: { async generateToken() { - os.popup(await import('@/components/token-generate-window.vue'), { + os.modal(await import('@/components/token-generate-window.vue'), { }).$on('ok', async ({ name, permissions }) => { const { token } = await os.api('miauth/gen-token', { session: null, diff --git a/src/client/pages/my-settings/index.vue b/src/client/pages/my-settings/index.vue index c20ccae65a..905d2ac214 100644 --- a/src/client/pages/my-settings/index.vue +++ b/src/client/pages/my-settings/index.vue @@ -114,7 +114,7 @@ export default defineComponent({ }, async configure() { - os.popup(await import('@/components/notification-setting-window.vue'), { + os.modal(await import('@/components/notification-setting-window.vue'), { includingTypes: this.$store.state.i.includingNotificationTypes, showGlobalToggle: false, }).$on('ok', async ({ includingTypes: value }: any) => { diff --git a/src/client/pages/my-settings/reaction.vue b/src/client/pages/my-settings/reaction.vue index 586a9e330d..e4f57c5123 100644 --- a/src/client/pages/my-settings/reaction.vue +++ b/src/client/pages/my-settings/reaction.vue @@ -58,7 +58,7 @@ export default defineComponent({ }, preview(ev) { - const picker = os.popup(MkReactionPicker, { + const picker = os.modal(MkReactionPicker, { source: ev.currentTarget || ev.target, reactions: this.splited, showFocus: false, @@ -73,7 +73,7 @@ export default defineComponent({ }, async chooseEmoji(ev) { - const vm = os.popup(await import('@/components/emoji-picker.vue'), { + const vm = os.modal(await import('@/components/emoji-picker.vue'), { source: ev.currentTarget || ev.target }).$once('chosen', emoji => { this.reactions += emoji; diff --git a/src/client/pages/preferences/plugins.vue b/src/client/pages/preferences/plugins.vue index 6f4df5aed3..d79b884194 100644 --- a/src/client/pages/preferences/plugins.vue +++ b/src/client/pages/preferences/plugins.vue @@ -118,7 +118,7 @@ export default defineComponent({ } const token = permissions == null || permissions.length === 0 ? null : await new Promise(async (res, rej) => { - os.popup(await import('@/components/token-generate-window.vue'), { + os.modal(await import('@/components/token-generate-window.vue'), { title: this.$t('tokenRequested'), information: this.$t('pluginTokenRequestedDescription'), initialName: name, diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue index 809a07876c..20b763d70c 100644 --- a/src/client/pages/share.vue +++ b/src/client/pages/share.vue @@ -60,7 +60,7 @@ export default defineComponent({ if (this.title) text += `【${this.title}】\n`; if (this.text) text += `${this.text}\n`; if (this.url) text += `${this.url}`; - os.popup(PostFormDialog, { + os.modal(PostFormDialog, { instant: true, initialText: text.trim() }).$once('posted', () => { diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue index 7a2ca72564..c1ddba8d1f 100644 --- a/src/client/pages/user/index.vue +++ b/src/client/pages/user/index.vue @@ -189,7 +189,7 @@ export default defineComponent({ }, menu() { - os.popup(XUserMenu, { + os.modal(XUserMenu, { source: this.$refs.menu, user: this.user }); diff --git a/src/client/root.vue b/src/client/root.vue index 41d295092d..da75cc862c 100644 --- a/src/client/root.vue +++ b/src/client/root.vue @@ -2,13 +2,17 @@ <DeckUI v-if="deckmode"/> <DefaultUI v-else/> -<component v-for="popup in $store.state.popups" :is="popup.component" v-bind="popup.props" :key="popup.id" @done="popup.done" @closed="popup.closed"/> +<XModal v-for="modal in $store.state.popups.filter(x => x.type === 'modal')" :key="modal.id" @closed="modal.closed" @click="modal.bgClick" :showing="modal.showing" :source="modal.source"> + <component :is="modal.component" v-bind="modal.props" @done="modal.done"/> +</XModal> + +<component v-for="popup in $store.state.popups.filter(x => x.type === 'popup')" :key="popup.id" :is="popup.component" v-bind="popup.props" @done="popup.done" @closed="popup.closed"/> <div id="wait" v-if="$store.state.pendingApiRequestsCount > 0"></div> </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { defineAsyncComponent, defineComponent } from 'vue'; import DefaultUI from './default.vue'; import DeckUI from './deck.vue'; import { instanceName, deckmode } from '@/config'; @@ -17,6 +21,7 @@ export default defineComponent({ components: { DefaultUI, DeckUI, + XModal: defineAsyncComponent(() => import('@/components/modal.vue')) }, metaInfo: { diff --git a/src/client/store.ts b/src/client/store.ts index c2a2b98068..f16d9426af 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -111,6 +111,7 @@ export const store = createStore({ popups: [] as { id: any; component: any; + type: 'popup' | 'modal', props: Record<string, any>; }[], fullView: false, diff --git a/src/client/widgets/notifications.vue b/src/client/widgets/notifications.vue index 71b56b3808..37a8391b84 100644 --- a/src/client/widgets/notifications.vue +++ b/src/client/widgets/notifications.vue @@ -51,7 +51,7 @@ export default defineComponent({ methods: { async configure() { - os.popup(await import('@/components/notification-setting-window.vue'), { + os.modal(await import('@/components/notification-setting-window.vue'), { includingTypes: this.props.includingTypes, }).$on('ok', async ({ includingTypes }) => { this.props.includingTypes = includingTypes; diff --git a/src/client/widgets/welcome.vue b/src/client/widgets/welcome.vue index b8ca7664f1..3f239cd5bf 100644 --- a/src/client/widgets/welcome.vue +++ b/src/client/widgets/welcome.vue @@ -52,13 +52,13 @@ export default defineComponent({ methods: { signin() { - os.popup(XSigninDialog, { + os.modal(XSigninDialog, { autoSet: true }); }, signup() { - os.popup(XSignupDialog, { + os.modal(XSignupDialog, { autoSet: true }); }