Skip to content
Snippets Groups Projects
textarea.vue 4.31 KiB
Newer Older
syuilo's avatar
syuilo committed
<template>
syuilo's avatar
syuilo committed
<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>
syuilo's avatar
syuilo committed
	</div>
syuilo's avatar
syuilo committed
	<div class="caption"><slot name="caption"></slot></div>
syuilo's avatar
syuilo committed

syuilo's avatar
syuilo committed
	<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</div>
syuilo's avatar
syuilo committed
</template>

<script lang="ts">
syuilo's avatar
syuilo committed
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
syuilo's avatar
syuilo committed
import MkButton from '@client/components/ui/button.vue';
syuilo's avatar
syuilo committed
import { debounce } from 'throttle-debounce';
syuilo's avatar
syuilo committed

export default defineComponent({
syuilo's avatar
syuilo committed
	components: {
syuilo's avatar
syuilo committed
		MkButton,
syuilo's avatar
syuilo committed
	},
syuilo's avatar
syuilo committed

syuilo's avatar
syuilo committed
	props: {
syuilo's avatar
syuilo committed
		modelValue: {
			required: true
		},
		type: {
			type: String,
syuilo's avatar
syuilo committed
			required: false
		},
		required: {
			type: Boolean,
			required: false
		},
		readonly: {
			type: Boolean,
			required: false
		},
syuilo's avatar
syuilo committed
		disabled: {
			type: Boolean,
			required: false
		},
syuilo's avatar
syuilo committed
		pattern: {
			type: String,
			required: false
		},
syuilo's avatar
syuilo committed
		placeholder: {
syuilo's avatar
syuilo committed
			type: String,
			required: false
		},
syuilo's avatar
syuilo committed
		autofocus: {
			type: Boolean,
			required: false,
			default: false
		},
		autocomplete: {
			required: false
		},
		spellcheck: {
			required: false
		},
syuilo's avatar
syuilo committed
		code: {
			type: Boolean,
			required: false
		},
		tall: {
			type: Boolean,
			required: false,
			default: false
		},
		pre: {
			type: Boolean,
			required: false,
			default: false
		},
syuilo's avatar
syuilo committed
		debounce: {
			type: Boolean,
			required: false,
			default: false
		},
syuilo's avatar
syuilo committed
		manualSave: {
			type: Boolean,
syuilo's avatar
syuilo committed
			required: false,
syuilo's avatar
syuilo committed
			default: false
syuilo's avatar
syuilo committed
		},
	},
syuilo's avatar
syuilo committed

	emits: ['change', 'keydown', 'enter', 'update:modelValue'],

syuilo's avatar
syuilo committed
	setup(props, context) {
syuilo's avatar
syuilo committed
		const { modelValue, autofocus } = toRefs(props);
		const v = ref(modelValue.value);
		const focused = ref(false);
syuilo's avatar
syuilo committed
		const changed = ref(false);
syuilo's avatar
syuilo committed
		const invalid = ref(false);
		const filled = computed(() => v.value !== '' && v.value != null);
syuilo's avatar
syuilo committed
		const inputEl = ref(null);
syuilo's avatar
syuilo committed

syuilo's avatar
syuilo committed
		const focus = () => inputEl.value.focus();
		const onInput = (ev) => {
			changed.value = true;
			context.emit('change', ev);
		};
syuilo's avatar
syuilo committed
		const onKeydown = (ev: KeyboardEvent) => {
			context.emit('keydown', ev);

			if (ev.code === 'Enter') {
				context.emit('enter');
			}
		};
syuilo's avatar
syuilo committed

		const updated = () => {
			changed.value = false;
syuilo's avatar
syuilo committed
			context.emit('update:modelValue', v.value);
syuilo's avatar
syuilo committed
		const debouncedUpdated = debounce(1000, updated);

		watch(modelValue, newValue => {
syuilo's avatar
syuilo committed
			v.value = newValue;
		});

		watch(v, newValue => {
			if (!props.manualSave) {
syuilo's avatar
syuilo committed
				if (props.debounce) {
					debouncedUpdated();
				} else {
					updated();
				}
syuilo's avatar
syuilo committed
			}
syuilo's avatar
syuilo committed

			invalid.value = inputEl.value.validity.badInput;
syuilo's avatar
syuilo committed
		});
syuilo's avatar
syuilo committed

		onMounted(() => {
			nextTick(() => {
				if (autofocus.value) {
					focus();
				}
			});
		});

syuilo's avatar
syuilo committed
		return {
syuilo's avatar
syuilo committed
			v,
syuilo's avatar
syuilo committed
			focused,
			invalid,
syuilo's avatar
syuilo committed
			changed,
syuilo's avatar
syuilo committed
			filled,
			inputEl,
syuilo's avatar
syuilo committed
			focus,
			onInput,
syuilo's avatar
syuilo committed
			onKeydown,
			updated,
syuilo's avatar
syuilo committed
		};
syuilo's avatar
syuilo committed
	},
syuilo's avatar
syuilo committed
});
</script>

<style lang="scss" scoped>
syuilo's avatar
syuilo committed
.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;
		}
	}
syuilo's avatar
syuilo committed

	> .input {
		position: relative;
syuilo's avatar
syuilo committed

syuilo's avatar
syuilo committed
		> textarea {
syuilo's avatar
syuilo committed
			appearance: none;
			-webkit-appearance: none;
syuilo's avatar
syuilo committed
			display: block;
			width: 100%;
			min-width: 100%;
			max-width: 100%;
			min-height: 130px;
			margin: 0;
syuilo's avatar
syuilo committed
			padding: 12px;
syuilo's avatar
syuilo committed
			font: inherit;
			font-weight: normal;
			font-size: 1em;
syuilo's avatar
syuilo committed
			color: var(--fg);
			background: var(--panel);
			border: solid 0.5px var(--inputBorder);
			border-radius: 6px;
syuilo's avatar
syuilo committed
			outline: none;
			box-shadow: none;
syuilo's avatar
syuilo committed
			box-sizing: border-box;
			transition: border-color 0.1s ease-out;
syuilo's avatar
syuilo committed

syuilo's avatar
syuilo committed
			&:hover {
				border-color: var(--inputBorderHover);
			}
		}

		&.focused {
			> textarea {
				border-color: var(--accent);
syuilo's avatar
syuilo committed
		&.disabled {
			opacity: 0.7;

			&, * {
				cursor: not-allowed !important;
			}
		}

		&.tall {
syuilo's avatar
syuilo committed
			> textarea {
				min-height: 200px;
			}
		}

syuilo's avatar
syuilo committed
		&.pre {
syuilo's avatar
syuilo committed
			> textarea {
				white-space: pre;
			}
		}
	}
}
</style>