diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue index fad0cf1593..d8f898ee13 100644 --- a/packages/client/src/components/poll-editor.vue +++ b/packages/client/src/components/poll-editor.vue @@ -3,7 +3,7 @@ <p v-if="choices.length < 2" class="caution"> <i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }} </p> - <ul ref="choices"> + <ul> <li v-for="(choice, i) in choices" :key="i"> <MkInput class="input" :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)"> </MkInput> @@ -14,8 +14,8 @@ </ul> <MkButton v-if="choices.length < 10" class="add" @click="add">{{ $ts.add }}</MkButton> <MkButton v-else class="add" disabled>{{ $ts._poll.noMore }}</MkButton> + <MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch> <section> - <MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch> <div> <MkSelect v-model="expiration"> <template #label>{{ $ts._poll.expiration }}</template> @@ -31,7 +31,7 @@ <template #label>{{ $ts._poll.deadlineTime }}</template> </MkInput> </section> - <section v-if="expiration === 'after'"> + <section v-else-if="expiration === 'after'"> <MkInput v-model="after" type="number" class="input"> <template #label>{{ $ts._poll.duration }}</template> </MkInput> @@ -47,8 +47,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { ref, watch } from 'vue'; import { addTime } from '@/scripts/time'; import { formatDateTimeString } from '@/scripts/format-time-string'; import MkInput from './form/input.vue'; @@ -56,125 +56,85 @@ import MkSelect from './form/select.vue'; import MkSwitch from './form/switch.vue'; import MkButton from './ui/button.vue'; -export default defineComponent({ - components: { - MkInput, - MkSelect, - MkSwitch, - MkButton, - }, +const props = defineProps<{ + modelValue: { + expiresAt: string; + expiredAfter: number; + choices: string[]; + multiple: boolean; + }; +}>(); +const emit = defineEmits<{ + (ev: 'update:modelValue', v: { + expiresAt: string; + expiredAfter: number; + choices: string[]; + multiple: boolean; + }): void; +}>(); - props: { - poll: { - type: Object, - required: true +const choices = ref(props.modelValue.choices); +const multiple = ref(props.modelValue.multiple); +const expiration = ref('infinite'); +const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd')); +const atTime = ref('00:00'); +const after = ref(0); +const unit = ref('second'); + +if (props.modelValue.expiresAt) { + expiration.value = 'at'; + atDate.value = atTime.value = props.modelValue.expiresAt; +} else if (typeof props.modelValue.expiredAfter === 'number') { + expiration.value = 'after'; + after.value = props.modelValue.expiredAfter / 1000; +} else { + expiration.value = 'infinite'; +} + +function onInput(i, value) { + choices.value[i] = value; +} + +function add() { + choices.value.push(''); + // TODO + // nextTick(() => { + // (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); + // }); +} + +function remove(i) { + choices.value = choices.value.filter((_, _i) => _i != i); +} + +function get() { + const calcAt = () => { + return new Date(`${atDate.value} ${atTime.value}`).getTime(); + }; + + const calcAfter = () => { + let base = parseInt(after.value); + switch (unit.value) { + case 'day': base *= 24; + case 'hour': base *= 60; + case 'minute': base *= 60; + case 'second': return base *= 1000; + default: return null; } - }, + }; - emits: ['updated'], + return { + choices: choices.value, + multiple: multiple.value, + ...( + expiration.value === 'at' ? { expiresAt: calcAt() } : + expiration.value === 'after' ? { expiredAfter: calcAfter() } : {} + ) + }; +} - data() { - return { - choices: this.poll.choices, - multiple: this.poll.multiple, - expiration: 'infinite', - atDate: formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'), - atTime: '00:00', - after: 0, - unit: 'second', - }; - }, - - watch: { - choices: { - handler() { - this.$emit('updated', this.get()); - }, - deep: true - }, - multiple: { - handler() { - this.$emit('updated', this.get()); - }, - }, - expiration: { - handler() { - this.$emit('updated', this.get()); - }, - }, - atDate: { - handler() { - this.$emit('updated', this.get()); - }, - }, - after: { - handler() { - this.$emit('updated', this.get()); - }, - }, - unit: { - handler() { - this.$emit('updated', this.get()); - }, - }, - }, - - created() { - const poll = this.poll; - if (poll.expiresAt) { - this.expiration = 'at'; - this.atDate = this.atTime = poll.expiresAt; - } else if (typeof poll.expiredAfter === 'number') { - this.expiration = 'after'; - this.after = poll.expiredAfter / 1000; - } else { - this.expiration = 'infinite'; - } - }, - - methods: { - onInput(i, e) { - this.choices[i] = e; - }, - - add() { - this.choices.push(''); - this.$nextTick(() => { - // TODO - //(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); - }); - }, - - remove(i) { - this.choices = this.choices.filter((_, _i) => _i != i); - }, - - get() { - const at = () => { - return new Date(`${this.atDate} ${this.atTime}`).getTime(); - }; - - const after = () => { - let base = parseInt(this.after); - switch (this.unit) { - case 'day': base *= 24; - case 'hour': base *= 60; - case 'minute': base *= 60; - case 'second': return base *= 1000; - default: return null; - } - }; - - return { - choices: this.choices, - multiple: this.multiple, - ...( - this.expiration === 'at' ? { expiresAt: at() } : - this.expiration === 'after' ? { expiredAfter: after() } : {} - ) - }; - }, - } +watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), { + deep: true, }); </script> @@ -216,7 +176,7 @@ export default defineComponent({ } > .add { - margin: 8px 0 0 0; + margin: 8px 0; z-index: 1; } @@ -225,21 +185,27 @@ export default defineComponent({ > div { margin: 0 8px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 12px; &:last-child { flex: 1 0 auto; - > section { - align-items: center; - display: flex; - margin: -32px 0 0; + > div { + flex-grow: 1; + } - > &:first-child { - margin-right: 16px; - } + > section { + // MAGIC: Prevent div above from growing unless wrapped to its own line + flex-grow: 9999; + align-items: end; + display: flex; + gap: 4px; > .input { - flex: 1 0 auto; + flex: 1 1 auto; } } } diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 0dcec26932..ed78c5a3fb 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -43,7 +43,7 @@ <textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.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"/> + <XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <XNotePreview v-if="showPreview" class="preview" :text="text"/> <footer> <button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button> @@ -111,9 +111,9 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'posted'): void; - (e: 'cancel'): void; - (e: 'esc'): void; + (ev: 'posted'): void; + (ev: 'cancel'): void; + (ev: 'esc'): void; }>(); const textareaEl = $ref<HTMLTextAreaElement | null>(null); @@ -127,8 +127,8 @@ let files = $ref(props.initialFiles ?? []); let poll = $ref<{ choices: string[]; multiple: boolean; - expiresAt: string; - expiredAfter: string; + expiresAt: string | null; + expiredAfter: string | null; } | null>(null); let useCw = $ref(false); let showPreview = $ref(false); @@ -371,11 +371,6 @@ function upload(file: File, name?: string) { }); } -function onPollUpdate(poll) { - poll = poll; - saveDraft(); -} - function setVisibility() { if (props.channel) { // TODO: information dialog