enhance: Improve poll-editor UI + composition port (#8186)

* Poll editor UI changes

Use a horizontal layout when possible, wrap to vertical when constrained

* Port poll-editor to composition API

* Fix poll-editor `get` time calcs

* fix

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
Derek 2022-01-25 11:26:12 -07:00 committed by GitHub
parent 65a19f0c75
commit 4e1974c6e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 101 additions and 140 deletions

View File

@ -3,7 +3,7 @@
<p v-if="choices.length < 2" class="caution"> <p v-if="choices.length < 2" class="caution">
<i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }} <i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }}
</p> </p>
<ul ref="choices"> <ul>
<li v-for="(choice, i) in choices" :key="i"> <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 class="input" :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
</MkInput> </MkInput>
@ -14,8 +14,8 @@
</ul> </ul>
<MkButton v-if="choices.length < 10" class="add" @click="add">{{ $ts.add }}</MkButton> <MkButton v-if="choices.length < 10" class="add" @click="add">{{ $ts.add }}</MkButton>
<MkButton v-else class="add" disabled>{{ $ts._poll.noMore }}</MkButton> <MkButton v-else class="add" disabled>{{ $ts._poll.noMore }}</MkButton>
<section>
<MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch> <MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
<section>
<div> <div>
<MkSelect v-model="expiration"> <MkSelect v-model="expiration">
<template #label>{{ $ts._poll.expiration }}</template> <template #label>{{ $ts._poll.expiration }}</template>
@ -31,7 +31,7 @@
<template #label>{{ $ts._poll.deadlineTime }}</template> <template #label>{{ $ts._poll.deadlineTime }}</template>
</MkInput> </MkInput>
</section> </section>
<section v-if="expiration === 'after'"> <section v-else-if="expiration === 'after'">
<MkInput v-model="after" type="number" class="input"> <MkInput v-model="after" type="number" class="input">
<template #label>{{ $ts._poll.duration }}</template> <template #label>{{ $ts._poll.duration }}</template>
</MkInput> </MkInput>
@ -47,8 +47,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { ref, watch } from 'vue';
import { addTime } from '@/scripts/time'; import { addTime } from '@/scripts/time';
import { formatDateTimeString } from '@/scripts/format-time-string'; import { formatDateTimeString } from '@/scripts/format-time-string';
import MkInput from './form/input.vue'; import MkInput from './form/input.vue';
@ -56,107 +56,65 @@ import MkSelect from './form/select.vue';
import MkSwitch from './form/switch.vue'; import MkSwitch from './form/switch.vue';
import MkButton from './ui/button.vue'; import MkButton from './ui/button.vue';
export default defineComponent({ const props = defineProps<{
components: { modelValue: {
MkInput, expiresAt: string;
MkSelect, expiredAfter: number;
MkSwitch, choices: string[];
MkButton, multiple: boolean;
},
props: {
poll: {
type: Object,
required: true
}
},
emits: ['updated'],
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',
}; };
}, }>();
const emit = defineEmits<{
(ev: 'update:modelValue', v: {
expiresAt: string;
expiredAfter: number;
choices: string[];
multiple: boolean;
}): void;
}>();
watch: { const choices = ref(props.modelValue.choices);
choices: { const multiple = ref(props.modelValue.multiple);
handler() { const expiration = ref('infinite');
this.$emit('updated', this.get()); const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
}, const atTime = ref('00:00');
deep: true const after = ref(0);
}, const unit = ref('second');
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() { if (props.modelValue.expiresAt) {
const poll = this.poll; expiration.value = 'at';
if (poll.expiresAt) { atDate.value = atTime.value = props.modelValue.expiresAt;
this.expiration = 'at'; } else if (typeof props.modelValue.expiredAfter === 'number') {
this.atDate = this.atTime = poll.expiresAt; expiration.value = 'after';
} else if (typeof poll.expiredAfter === 'number') { after.value = props.modelValue.expiredAfter / 1000;
this.expiration = 'after';
this.after = poll.expiredAfter / 1000;
} else { } else {
this.expiration = 'infinite'; expiration.value = 'infinite';
} }
},
methods: { function onInput(i, value) {
onInput(i, e) { choices.value[i] = value;
this.choices[i] = e; }
},
add() { function add() {
this.choices.push(''); choices.value.push('');
this.$nextTick(() => {
// TODO // TODO
// nextTick(() => {
// (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); // (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
}); // });
}, }
remove(i) { function remove(i) {
this.choices = this.choices.filter((_, _i) => _i != i); choices.value = choices.value.filter((_, _i) => _i != i);
}, }
get() { function get() {
const at = () => { const calcAt = () => {
return new Date(`${this.atDate} ${this.atTime}`).getTime(); return new Date(`${atDate.value} ${atTime.value}`).getTime();
}; };
const after = () => { const calcAfter = () => {
let base = parseInt(this.after); let base = parseInt(after.value);
switch (this.unit) { switch (unit.value) {
case 'day': base *= 24; case 'day': base *= 24;
case 'hour': base *= 60; case 'hour': base *= 60;
case 'minute': base *= 60; case 'minute': base *= 60;
@ -166,15 +124,17 @@ export default defineComponent({
}; };
return { return {
choices: this.choices, choices: choices.value,
multiple: this.multiple, multiple: multiple.value,
...( ...(
this.expiration === 'at' ? { expiresAt: at() } : expiration.value === 'at' ? { expiresAt: calcAt() } :
this.expiration === 'after' ? { expiredAfter: after() } : {} expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
) )
}; };
},
} }
watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), {
deep: true,
}); });
</script> </script>
@ -216,7 +176,7 @@ export default defineComponent({
} }
> .add { > .add {
margin: 8px 0 0 0; margin: 8px 0;
z-index: 1; z-index: 1;
} }
@ -225,21 +185,27 @@ export default defineComponent({
> div { > div {
margin: 0 8px; margin: 0 8px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 12px;
&:last-child { &:last-child {
flex: 1 0 auto; flex: 1 0 auto;
> section { > div {
align-items: center; flex-grow: 1;
display: flex;
margin: -32px 0 0;
> &: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 { > .input {
flex: 1 0 auto; flex: 1 1 auto;
} }
} }
} }

View File

@ -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"/> <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"> <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"/> <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"/> <XNotePreview v-if="showPreview" class="preview" :text="text"/>
<footer> <footer>
<button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button> <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<{ const emit = defineEmits<{
(e: 'posted'): void; (ev: 'posted'): void;
(e: 'cancel'): void; (ev: 'cancel'): void;
(e: 'esc'): void; (ev: 'esc'): void;
}>(); }>();
const textareaEl = $ref<HTMLTextAreaElement | null>(null); const textareaEl = $ref<HTMLTextAreaElement | null>(null);
@ -127,8 +127,8 @@ let files = $ref(props.initialFiles ?? []);
let poll = $ref<{ let poll = $ref<{
choices: string[]; choices: string[];
multiple: boolean; multiple: boolean;
expiresAt: string; expiresAt: string | null;
expiredAfter: string; expiredAfter: string | null;
} | null>(null); } | null>(null);
let useCw = $ref(false); let useCw = $ref(false);
let showPreview = $ref(false); let showPreview = $ref(false);
@ -371,11 +371,6 @@ function upload(file: File, name?: string) {
}); });
} }
function onPollUpdate(poll) {
poll = poll;
saveDraft();
}
function setVisibility() { function setVisibility() {
if (props.channel) { if (props.channel) {
// TODO: information dialog // TODO: information dialog