147 lines
3.7 KiB
Vue
147 lines
3.7 KiB
Vue
<script setup lang="ts">
|
|
import axios from 'axios'
|
|
import { useVModel, watchDebounced, useTextareaAutosize, syncRef } from '@vueuse/core'
|
|
import { ref, computed, watchEffect, onMounted, nextTick, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import useLogger from '~/composables/useLogger'
|
|
|
|
interface Events {
|
|
(e: 'update:modelValue', value: string): void
|
|
}
|
|
|
|
interface Props {
|
|
modelValue: string
|
|
placeholder?: string
|
|
autofocus?: boolean
|
|
permissive?: boolean
|
|
required?: boolean
|
|
charLimit?: number
|
|
}
|
|
|
|
const emit = defineEmits<Events>()
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
placeholder: undefined,
|
|
autofocus: false,
|
|
charLimit: 5000,
|
|
permissive: false,
|
|
required: false
|
|
})
|
|
|
|
const logger = useLogger()
|
|
|
|
const { t } = useI18n()
|
|
const { textarea, input } = useTextareaAutosize()
|
|
const value = useVModel(props, 'modelValue', emit)
|
|
syncRef(value, input)
|
|
|
|
const isPreviewing = ref(false)
|
|
const preview = ref()
|
|
const isLoadingPreview = ref(false)
|
|
|
|
const labels = computed(() => ({
|
|
placeholder: props.placeholder ?? t('components.common.ContentForm.placeholder.input')
|
|
}))
|
|
|
|
const remainingChars = computed(() => props.charLimit - props.modelValue.length)
|
|
|
|
const loadPreview = async () => {
|
|
isLoadingPreview.value = true
|
|
try {
|
|
const response = await axios.post('text-preview/', { text: value.value, permissive: props.permissive })
|
|
preview.value = response.data.rendered
|
|
} catch (error) {
|
|
logger.error(error)
|
|
}
|
|
isLoadingPreview.value = false
|
|
}
|
|
|
|
watchDebounced(value, async () => {
|
|
await loadPreview()
|
|
}, { immediate: true, debounce: 500 })
|
|
|
|
watchEffect(async () => {
|
|
if (isPreviewing.value) {
|
|
if (value.value && !preview.value && !isLoadingPreview.value) {
|
|
await loadPreview()
|
|
}
|
|
}
|
|
})
|
|
|
|
watch(isPreviewing, (to, from) => {
|
|
if (from === true) {
|
|
textarea.value.focus()
|
|
}
|
|
}, { flush: 'post' })
|
|
|
|
onMounted(async () => {
|
|
if (props.autofocus) {
|
|
await nextTick()
|
|
textarea.value.focus()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="content-form ui segments">
|
|
<div class="ui segment">
|
|
<div class="ui tiny secondary pointing menu">
|
|
<button
|
|
:class="[{active: !isPreviewing}, 'item']"
|
|
@click.prevent="isPreviewing = false"
|
|
>
|
|
{{ $t('components.common.ContentForm.button.write') }}
|
|
</button>
|
|
<button
|
|
:class="[{active: isPreviewing}, 'item']"
|
|
@click.prevent="isPreviewing = true"
|
|
>
|
|
{{ $t('components.common.ContentForm.button.preview') }}
|
|
</button>
|
|
</div>
|
|
<template v-if="isPreviewing">
|
|
<div
|
|
v-if="isLoadingPreview"
|
|
class="ui placeholder"
|
|
>
|
|
<div class="paragraph">
|
|
<div class="line" />
|
|
<div class="line" />
|
|
<div class="line" />
|
|
<div class="line" />
|
|
</div>
|
|
</div>
|
|
<p v-else-if="!preview">
|
|
{{ $t('components.common.ContentForm.empty.noContent') }}
|
|
</p>
|
|
<sanitized-html
|
|
v-else
|
|
:html="preview"
|
|
/>
|
|
</template>
|
|
<template v-else>
|
|
<div class="ui transparent input">
|
|
<textarea
|
|
ref="textarea"
|
|
v-model="value"
|
|
:required="required"
|
|
:placeholder="labels.placeholder"
|
|
/>
|
|
</div>
|
|
<div class="ui very small hidden divider" />
|
|
</template>
|
|
</div>
|
|
<div class="ui bottom attached segment">
|
|
<span
|
|
v-if="charLimit"
|
|
:class="['right', 'floated', {'ui danger text': remainingChars < 0}]"
|
|
>
|
|
{{ remainingChars }}
|
|
</span>
|
|
<p>
|
|
{{ $t('components.common.ContentForm.help.markdown') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|