diff options
Diffstat (limited to 'src/client/components/form/textarea.vue')
| -rw-r--r-- | src/client/components/form/textarea.vue | 195 |
1 files changed, 143 insertions, 52 deletions
diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue index 8f42581a9b..50be69f930 100644 --- a/src/client/components/form/textarea.vue +++ b/src/client/components/form/textarea.vue @@ -1,40 +1,45 @@ <template> -<FormGroup class="_formItem"> - <template #label><slot></slot></template> - <div class="rivhosbp _formItem" :class="{ tall, pre }"> - <div class="input _formPanel"> - <textarea ref="input" :class="{ code, _monospace: code }" - v-model="v" - :required="required" - :readonly="readonly" - :pattern="pattern" - :autocomplete="autocomplete" - :spellcheck="!code" - @input="onInput" - @focus="focused = true" - @blur="focused = false" - ></textarea> - </div> +<div class="adhpbeos"> + <div class="label" @click="focus"><slot name="label"></slot></div> + <div class="input" :class="{ disabled, focused, tall, pre }"> + <textarea ref="inputEl" + :class="{ code, _monospace: code }" + v-model="v" + :disabled="disabled" + :required="required" + :readonly="readonly" + :placeholder="placeholder" + :pattern="pattern" + :autocomplete="autocomplete" + :spellcheck="spellcheck" + @focus="focused = true" + @blur="focused = false" + @keydown="onKeydown($event)" + @input="onInput" + ></textarea> </div> - <template #caption><slot name="desc"></slot></template> + <div class="caption"><slot name="caption"></slot></div> - <FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> -</FormGroup> + <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> +</div> </template> <script lang="ts"> -import { defineComponent, ref, toRefs, watch } from 'vue'; -import './form.scss'; -import FormButton from './button.vue'; -import FormGroup from './group.vue'; +import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; +import MkButton from '../ui/button.vue'; +import { debounce } from 'throttle-debounce'; export default defineComponent({ components: { - FormGroup, - FormButton, + MkButton, }, + props: { - value: { + modelValue: { + required: true + }, + type: { + type: String, required: false }, required: { @@ -45,14 +50,29 @@ export default defineComponent({ type: Boolean, required: false }, + disabled: { + type: Boolean, + required: false + }, pattern: { type: String, required: false }, - autocomplete: { + placeholder: { type: String, required: false }, + autofocus: { + type: Boolean, + required: false, + default: false + }, + autocomplete: { + required: false + }, + spellcheck: { + required: false + }, code: { type: Boolean, required: false @@ -67,91 +87,162 @@ export default defineComponent({ required: false, default: false }, + debounce: { + type: Boolean, + required: false, + default: false + }, manualSave: { type: Boolean, required: false, default: false }, }, + + emits: ['change', 'keydown', 'enter', 'update:modelValue'], + setup(props, context) { - const { value } = toRefs(props); - const v = ref(value.value); + const { modelValue, autofocus } = toRefs(props); + const v = ref(modelValue.value); + const focused = ref(false); const changed = ref(false); + const invalid = ref(false); + const filled = computed(() => v.value !== '' && v.value != null); const inputEl = ref(null); + const focus = () => inputEl.value.focus(); const onInput = (ev) => { changed.value = true; context.emit('change', ev); }; + const onKeydown = (ev: KeyboardEvent) => { + context.emit('keydown', ev); + + if (ev.code === 'Enter') { + context.emit('enter'); + } + }; const updated = () => { changed.value = false; - context.emit('update:value', v.value); + context.emit('update:modelValue', v.value); }; - watch(value, newValue => { + const debouncedUpdated = debounce(1000, updated); + + watch(modelValue, newValue => { v.value = newValue; }); watch(v, newValue => { if (!props.manualSave) { - updated(); + if (props.debounce) { + debouncedUpdated(); + } else { + updated(); + } } + + invalid.value = inputEl.value.validity.badInput; }); - + + onMounted(() => { + nextTick(() => { + if (autofocus.value) { + focus(); + } + }); + }); + return { v, - updated, + focused, + invalid, changed, + filled, + inputEl, focus, onInput, + onKeydown, + updated, }; - } + }, }); </script> <style lang="scss" scoped> -.rivhosbp { - position: relative; +.adhpbeos { + > .label { + font-size: 0.85em; + padding: 0 0 8px 12px; + user-select: none; + + &:empty { + display: none; + } + } + + > .caption { + font-size: 0.8em; + padding: 8px 0 0 12px; + color: var(--fgTransparentWeak); + + &:empty { + display: none; + } + } > .input { position: relative; - + > textarea { + appearance: none; + -webkit-appearance: none; display: block; width: 100%; min-width: 100%; max-width: 100%; min-height: 130px; margin: 0; - padding: 16px; - box-sizing: border-box; + padding: 12px; font: inherit; font-weight: normal; font-size: 1em; - background: transparent; - border: none; - border-radius: 0; + color: var(--fg); + background: var(--panel); + border: solid 0.5px var(--inputBorder); + border-radius: 6px; outline: none; box-shadow: none; - color: var(--fg); + box-sizing: border-box; + transition: border-color 0.1s ease-out; - &.code { - tab-size: 2; + &:hover { + border-color: var(--inputBorderHover); + } + } + + &.focused { + > textarea { + border-color: var(--accent); } } - } - &.tall { - > .input { + &.disabled { + opacity: 0.7; + + &, * { + cursor: not-allowed !important; + } + } + + &.tall { > textarea { min-height: 200px; } } - } - &.pre { - > .input { + &.pre { > textarea { white-space: pre; } |