410 lines
9.7 KiB
Vue
410 lines
9.7 KiB
Vue
<template>
|
|
<div
|
|
:class="[
|
|
'sm-input-group',
|
|
{
|
|
'sm-input-active': inputActive,
|
|
'sm-feedback-invalid': feedbackInvalid,
|
|
},
|
|
computedClassType,
|
|
]">
|
|
<label v-if="label">{{ label }}</label>
|
|
<ion-icon
|
|
class="sm-invalid-icon"
|
|
name="alert-circle-outline"></ion-icon>
|
|
<input
|
|
v-if="
|
|
type == 'text' ||
|
|
type == 'email' ||
|
|
type == 'password' ||
|
|
type == 'email' ||
|
|
type == 'url' ||
|
|
type == 'daterange' ||
|
|
type == 'datetime'
|
|
"
|
|
:type="type"
|
|
:value="value"
|
|
@input="handleInput"
|
|
@focus="handleFocus"
|
|
@blur="handleBlur"
|
|
@keydown="handleKeydown" />
|
|
<textarea
|
|
v-else-if="type == 'textarea'"
|
|
rows="5"
|
|
:value="value"
|
|
@input="handleInput"
|
|
@focus="handleFocus"
|
|
@blur="handleBlur"
|
|
@keydown="handleKeydown"></textarea>
|
|
<div v-else-if="type == 'file'" class="input-file-group">
|
|
<input
|
|
id="file"
|
|
type="file"
|
|
class="file"
|
|
:accept="props.accept"
|
|
@change="handleChange" />
|
|
<label class="sm-button" for="file">Select file</label>
|
|
<div class="file-name">
|
|
{{ modelValue?.name ? modelValue.name : modelValue }}
|
|
</div>
|
|
</div>
|
|
<select
|
|
v-else-if="type == 'select'"
|
|
:value="value"
|
|
@input="handleInput"
|
|
@focus="handleFocus"
|
|
@blur="handleBlur"
|
|
@keydown="handleKeydown">
|
|
<option
|
|
v-for="(optionValue, key) in options"
|
|
:key="key"
|
|
:value="key"
|
|
:selected="key == value">
|
|
{{ optionValue }}
|
|
</option>
|
|
</select>
|
|
<div v-else-if="type == 'media'" class="sm-input-media">
|
|
<div class="sm-input-media-item">
|
|
<img v-if="mediaUrl.length > 0" :src="mediaUrl" />
|
|
<ion-icon v-else name="image-outline" />
|
|
</div>
|
|
<a
|
|
class="sm-button sm-button-small"
|
|
@click.prevent="handleMediaSelect"
|
|
>Select file</a
|
|
>
|
|
</div>
|
|
<div v-if="slots.default || feedbackInvalid" class="sm-input-help">
|
|
<span v-if="feedbackInvalid" class="sm-input-invalid">{{
|
|
feedbackInvalid
|
|
}}</span>
|
|
<span v-if="slots.default" class="sm-input-info">
|
|
<slot></slot>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, inject, ref, useSlots, watch } from "vue";
|
|
import { openDialog } from "vue3-promise-dialog";
|
|
import { toTitleCase } from "../helpers/string";
|
|
import { isEmpty } from "../helpers/utils";
|
|
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: String,
|
|
default: "",
|
|
required: false,
|
|
},
|
|
label: {
|
|
type: String,
|
|
default: "",
|
|
required: false,
|
|
},
|
|
type: {
|
|
type: String,
|
|
default: "text",
|
|
},
|
|
feedbackInvalid: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
accept: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
options: {
|
|
type: Object,
|
|
default() {
|
|
return {};
|
|
},
|
|
},
|
|
control: {
|
|
type: [String, Object],
|
|
default: "",
|
|
},
|
|
form: {
|
|
type: Object,
|
|
default: () => {
|
|
return {};
|
|
},
|
|
required: false,
|
|
},
|
|
});
|
|
|
|
const emits = defineEmits(["update:modelValue", "focus", "blur", "keydown"]);
|
|
const slots = useSlots();
|
|
const mediaUrl = ref("");
|
|
|
|
const objForm = inject("form", props.form);
|
|
const objControl =
|
|
typeof props.control == "object"
|
|
? props.control
|
|
: !isEmpty(objForm) &&
|
|
typeof props.control == "string" &&
|
|
props.control != ""
|
|
? objForm.controls[props.control]
|
|
: null;
|
|
|
|
const label = ref(props.label);
|
|
const feedbackInvalid = ref(props.feedbackInvalid);
|
|
const value = ref(props.modelValue);
|
|
const inputActive = ref(value.value.length > 0 || props.type == "select");
|
|
|
|
/**
|
|
* Return the classname based on type
|
|
*/
|
|
const computedClassType = computed(() => {
|
|
return `sm-input-${props.type}`;
|
|
});
|
|
|
|
watch(
|
|
() => props.label,
|
|
(newValue) => {
|
|
label.value = newValue;
|
|
}
|
|
);
|
|
|
|
if (objControl) {
|
|
if (value.value.length > 0) {
|
|
objControl.value = value.value;
|
|
} else {
|
|
value.value = objControl.value;
|
|
}
|
|
|
|
if (label.value.length == 0) {
|
|
label.value = toTitleCase(props.control);
|
|
}
|
|
|
|
inputActive.value = value.value.length > 0;
|
|
|
|
watch(
|
|
() => objControl.validation.result.valid,
|
|
(newValue) => {
|
|
feedbackInvalid.value = newValue
|
|
? ""
|
|
: objControl.validation.result.invalidMessages[0];
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
watch(
|
|
() => objControl.value,
|
|
(newValue) => {
|
|
value.value = newValue;
|
|
},
|
|
{ deep: true }
|
|
);
|
|
}
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(newValue) => {
|
|
value.value = newValue;
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => props.feedbackInvalid,
|
|
(newValue) => {
|
|
feedbackInvalid.value = newValue;
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => value.value,
|
|
(newValue) => {
|
|
inputActive.value = newValue.length > 0;
|
|
}
|
|
);
|
|
|
|
const handleChange = (event) => {
|
|
emits("update:modelValue", event.target.files[0]);
|
|
};
|
|
|
|
const handleInput = (event: Event) => {
|
|
const target = event.target as HTMLInputElement;
|
|
value.value = target.value;
|
|
emits("update:modelValue", target.value);
|
|
|
|
if (objControl) {
|
|
objControl.value = target.value;
|
|
feedbackInvalid.value = "";
|
|
}
|
|
};
|
|
|
|
const handleFocus = (event: Event) => {
|
|
inputActive.value = true;
|
|
|
|
if (event instanceof KeyboardEvent) {
|
|
if (event.key === undefined || event.key === "Tab") {
|
|
emits("blur", event);
|
|
}
|
|
}
|
|
|
|
emits("focus", event);
|
|
};
|
|
|
|
const handleBlur = async (event: Event) => {
|
|
if (objControl) {
|
|
objControl.validate();
|
|
objControl.isValid();
|
|
}
|
|
|
|
const target = event.target as HTMLInputElement;
|
|
|
|
if (target.value.length == 0) {
|
|
inputActive.value = false;
|
|
}
|
|
|
|
emits("blur", event);
|
|
};
|
|
|
|
const handleKeydown = (event: Event) => {
|
|
emits("keydown", event);
|
|
};
|
|
|
|
const handleMediaSelect = async (event) => {
|
|
let result = await openDialog(SMDialogMedia);
|
|
if (result) {
|
|
mediaUrl.value = result.url;
|
|
emits("update:modelValue", result.id);
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.sm-column > .sm-input-group {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.sm-input-group {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
margin-bottom: map-get($spacer, 4);
|
|
flex: 1;
|
|
|
|
&.sm-input-active {
|
|
label {
|
|
transform: translate(8px, -3px) scale(0.7);
|
|
color: $secondary-color-dark;
|
|
}
|
|
|
|
input {
|
|
padding: calc(#{map-get($spacer, 2)} * 1.5) map-get($spacer, 3)
|
|
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
|
|
}
|
|
|
|
textarea {
|
|
padding: calc(#{map-get($spacer, 2)} * 2) map-get($spacer, 3)
|
|
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
|
|
}
|
|
|
|
select {
|
|
padding: calc(#{map-get($spacer, 2)} * 2) map-get($spacer, 3)
|
|
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
|
|
}
|
|
}
|
|
|
|
&.sm-feedback-invalid {
|
|
input,
|
|
select,
|
|
textarea {
|
|
border: 2px solid $danger-color;
|
|
}
|
|
|
|
.sm-invalid-icon {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
label {
|
|
position: absolute;
|
|
display: block;
|
|
padding: map-get($spacer, 2) map-get($spacer, 3);
|
|
line-height: 1.5;
|
|
transform-origin: top left;
|
|
transform: translate(0, 1px) scale(1);
|
|
transition: all 0.1s ease-in-out;
|
|
color: $secondary-color-dark;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.sm-invalid-icon {
|
|
position: absolute;
|
|
display: none;
|
|
right: 0;
|
|
top: 2px;
|
|
padding: map-get($spacer, 2) map-get($spacer, 3);
|
|
color: $danger-color;
|
|
font-size: 120%;
|
|
}
|
|
|
|
input,
|
|
select,
|
|
textarea {
|
|
box-sizing: border-box;
|
|
display: block;
|
|
width: 100%;
|
|
border: 1px solid $border-color;
|
|
border-radius: 12px;
|
|
padding: map-get($spacer, 2) map-get($spacer, 3);
|
|
color: $font-color;
|
|
margin-bottom: map-get($spacer, 1);
|
|
|
|
-webkit-appearance: none;
|
|
-moz-appearance: none;
|
|
appearance: none;
|
|
}
|
|
|
|
textarea {
|
|
resize: none;
|
|
}
|
|
|
|
&.sm-input-media {
|
|
label {
|
|
position: relative;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
.sm-input-media {
|
|
text-align: center;
|
|
|
|
.sm-input-media-item {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
|
|
img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
}
|
|
|
|
ion-icon {
|
|
padding: 4rem;
|
|
font-size: 3rem;
|
|
border: 1px solid $border-color;
|
|
background-color: #fff;
|
|
}
|
|
}
|
|
|
|
.button {
|
|
display: inline-block;
|
|
}
|
|
}
|
|
|
|
.sm-input-help {
|
|
font-size: 75%;
|
|
margin: 0 map-get($spacer, 1);
|
|
|
|
.sm-input-invalid {
|
|
color: $danger-color;
|
|
padding-right: map-get($spacer, 1);
|
|
}
|
|
}
|
|
}
|
|
</style>
|