Compare commits

...

5 Commits

Author SHA1 Message Date
Derek 1121e9646d [ChatUI] Add preview to post form 2022-01-24 21:21:06 -07:00
Derek fce1172510 Fix poll-editor `get` time calcs 2022-01-24 20:32:39 -07:00
Derek 9bda942aad Port poll-editor to composition API 2022-01-24 20:01:01 -07:00
Derek c726a98abe Poll editor UI changes
Use a horizontal layout when possible, wrap to vertical when constrained
2022-01-24 19:24:07 -07:00
Derek 88522628e9 [ChatUI] Fix index route 2022-01-24 18:30:49 -07:00
3 changed files with 117 additions and 134 deletions

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="zmdxowus"> <div class="zmdxowus">
<p v-if="choices.length < 2" class="caution"> <p v-if="poll.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 ref="choices">
<li v-for="(choice, i) in choices" :key="i"> <li v-for="(choice, i) in poll.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>
<button class="_button" @click="remove(i)"> <button class="_button" @click="remove(i)">
@ -12,10 +12,10 @@
</button> </button>
</li> </li>
</ul> </ul>
<MkButton v-if="choices.length < 10" class="add" @click="add">{{ $ts.add }}</MkButton> <MkButton v-if="poll.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="poll.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>
@ -47,8 +47,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script 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,126 +56,73 @@ 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 { poll } = defineProps({
components: { poll: {
MkInput, type: Object,
MkSelect, required: true,
MkSwitch,
MkButton,
}, },
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',
};
},
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() } : {}
)
};
},
}
}); });
const emit = defineEmits(['updated']);
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 (poll.expiresAt) {
expiration.value = 'at';
atDate.value = atTime.value = poll.expiresAt;
} else if (typeof poll.expiredAfter === 'number') {
expiration.value = 'after';
after.value = poll.expiredAfter / 1000;
} else {
expiration.value = 'infinite';
}
function onInput(i, e) {
poll.choices[i] = e;
}
function add() {
poll.choices.push('');
// TODO
// nextTick(() => {
// (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
// });
}
function remove(i) {
poll.choices = poll.choices.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) {
case 'day': base *= 24;
case 'hour': base *= 60;
case 'minute': base *= 60;
case 'second': return base *= 1000;
default: return null;
}
};
return {
choices: poll.choices,
multiple: poll.multiple,
...(
expiration.value === 'at' ? { expiresAt: calcAt() } :
expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
)
};
}
watch([poll, expiration, atDate, after, unit], () => emit('updated', get()));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -216,7 +163,7 @@ export default defineComponent({
} }
> .add { > .add {
margin: 8px 0 0 0; margin: 8px 0;
z-index: 1; z-index: 1;
} }
@ -225,21 +172,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;
} }
} }
} }

View File

@ -89,6 +89,7 @@ const defaultRoutes = [
]; ];
const chatRoutes = [ const chatRoutes = [
{ path: '/', component: $i ? page('timeline', 'chat') : page('welcome'), props: $i ? route => ({ src: 'home' }) : undefined },
{ path: '/timeline', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) }, { path: '/timeline', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
{ path: '/timeline/home', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) }, { path: '/timeline/home', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
{ path: '/timeline/local', component: page('timeline', 'chat'), props: route => ({ src: 'local' }) }, { path: '/timeline/local', component: page('timeline', 'chat'), props: route => ({ src: 'local' }) },

View File

@ -21,6 +21,7 @@
<textarea ref="text" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <textarea ref="text" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<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" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
<XNotePreview v-if="showPreview" class="preview" :text="text"/>
<footer> <footer>
<div class="left"> <div class="left">
<button v-tooltip="$ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button> <button v-tooltip="$ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
@ -39,6 +40,7 @@
<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span> <span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span> <span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
</button> </button>
<button v-tooltip="$ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button> <button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
</div> </div>
</footer> </footer>
@ -51,6 +53,7 @@ import { defineComponent, defineAsyncComponent } from 'vue';
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
import { length } from 'stringz'; import { length } from 'stringz';
import { toASCII } from 'punycode/'; import { toASCII } from 'punycode/';
import XNotePreview from '@/components/note-preview.vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { host, url } from '@/config'; import { host, url } from '@/config';
import { erase, unique } from '@/scripts/array'; import { erase, unique } from '@/scripts/array';
@ -65,6 +68,7 @@ import { throttle } from 'throttle-debounce';
export default defineComponent({ export default defineComponent({
components: { components: {
XNotePreview,
XPostFormAttaches: defineAsyncComponent(() => import('@/components/post-form-attaches.vue')), XPostFormAttaches: defineAsyncComponent(() => import('@/components/post-form-attaches.vue')),
XPollEditor: defineAsyncComponent(() => import('@/components/poll-editor.vue')) XPollEditor: defineAsyncComponent(() => import('@/components/poll-editor.vue'))
}, },
@ -122,6 +126,7 @@ export default defineComponent({
cw: null, cw: null,
localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly, localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility, visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
showPreview: false,
visibleUsers: [], visibleUsers: [],
autocomplete: null, autocomplete: null,
draghover: false, draghover: false,
@ -417,6 +422,7 @@ export default defineComponent({
this.files = []; this.files = [];
this.poll = null; this.poll = null;
this.quoteId = null; this.quoteId = null;
this.showPreview = false;
}, },
onKeydown(e: KeyboardEvent) { onKeydown(e: KeyboardEvent) {
@ -735,7 +741,12 @@ export default defineComponent({
> .visibility { > .visibility {
width: $height; width: $height;
margin: 0 8px; margin: 0 0 0 8px;
border-radius: 6px;
&:hover {
background: var(--X5);
}
& + .localOnly { & + .localOnly {
margin-left: 0 !important; margin-left: 0 !important;
@ -747,12 +758,30 @@ export default defineComponent({
opacity: 0.7; opacity: 0.7;
} }
> .preview {
display: inline-block;
padding: 0;
margin: 0 8px 0 0;
font-size: 16px;
width: $height;
border-radius: 6px;
&:hover {
background: var(--X5);
}
&.active {
color: var(--accent);
}
}
> .submit { > .submit {
margin: 0; margin: 0;
padding: 0 12px; padding: 0 12px;
line-height: 34px; line-height: 34px;
font-weight: bold; font-weight: bold;
border-radius: 4px; border-radius: 4px;
font-size: 0.9em;
&:disabled { &:disabled {
opacity: 0.7; opacity: 0.7;