mirror of
https://github.com/kokonect-link/cherrypick
synced 2024-11-01 15:45:58 +09:00
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:
parent
65a19f0c75
commit
4e1974c6e6
@ -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>
|
||||||
|
<MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
|
||||||
<section>
|
<section>
|
||||||
<MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
|
|
||||||
<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,125 +56,85 @@ 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;
|
||||||
},
|
};
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'update:modelValue', v: {
|
||||||
|
expiresAt: string;
|
||||||
|
expiredAfter: number;
|
||||||
|
choices: string[];
|
||||||
|
multiple: boolean;
|
||||||
|
}): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
props: {
|
const choices = ref(props.modelValue.choices);
|
||||||
poll: {
|
const multiple = ref(props.modelValue.multiple);
|
||||||
type: Object,
|
const expiration = ref('infinite');
|
||||||
required: true
|
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() {
|
watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), {
|
||||||
return {
|
deep: true,
|
||||||
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() } : {}
|
|
||||||
)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</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 {
|
> section {
|
||||||
margin-right: 16px;
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user