DigitalConsumer.ISS/src/digital-components/textbox-question/textbox-question.vue
AHumphriesSL 0f9922e38d
Merge pull request #1074 from Safelite/feature/humphries/INSR-7752.1
INSR-8434: Fix defects seen on contact-details
2026-02-13 12:25:58 -05:00

412 lines
12 KiB
Vue

<template>
<div
class="textbox-question"
:class="[
((errors && errors.length) || hasError) ? 'has-error' : '',
(meta?.valid && meta?.touched && !((errors && errors.length) || hasError)) ? 'has-success' : ''
]">
<label
v-if="displayQuestionText"
:for="inputId"
:aria-label="questionTextForAria"
class="form-label"
:class="[questionAlignment === 'center' ? 'text-center w-100 mb-5' : '']"
v-html="questionText"></label>
<div
class="input-wrapper"
:class="[
includeSearchIcon ? 'has-search-icon' : '',
includeSelectIcon ? 'has-select-icon' : '',
hasTextButton ? 'has-text-button' : ''
]">
<input
:id="inputId"
:ref="inputId"
v-model.trim="value"
v-maska
:data-maska="getMaskFromProp"
:data-maska-tokens="getTokensFromProp"
class="form-control"
:type="type"
:name="inputId"
:placeholder="placeholderText"
:aria-disabled="isDisabled"
:disabled="isDisabled"
:aria-required="isRequired"
:aria-label="questionTextForAria"
:min="min"
:max="max"
:required="isRequired"
:class="[
hasIcon ? 'has-icon' : '',
iconRight ? 'icon-right' : '',
cornerStyle === 'rounded' ? 'rounded-pill' : ''
]"
:validationRules="validationRules"
:maxlength="maxLength ? maxLength : '999'"
:data-bs-toggle="includeSelectIcon ? 'modal' : ''"
:data-bs-target="'#' + cmsWidgetName"
@change="handleChange"
@blur="handleChange"
@focus="$emit('focus', $event.target.value)"
@paste="trimOnPaste"
@drop="trimOnPaste" />
<button
v-if="includeSearchIcon"
type="submit"
aria-label="Search button"
@click="clickedSearch" />
<button
v-if="includeSelectIcon"
type="submit"
data-bs-toggle="modal"
:data-bs-target="'#' + cmsWidgetName"
aria-label="Select button" />
<buttonMain
v-if="hasTextButton"
ref="buttonMain"
variant="primary"
:buttonText="buttonText"
@clickEvent="clickedTextButton">
</buttonMain>
</div>
<div
v-show="errorMessage"
ref="errorMessageDiv"
class="row my-1 form-test-error">
<span
class="d-inline-flex mt-0"
role="alert">{{ errorMessage }}</span>
</div>
</div>
</template>
<script>
import { useField, validate } from 'vee-validate';
import { extractSpanText } from '@/helpers/text-helper';
import buttonMain from '@/ux-components/button-main/button-main.vue';
export default {
name: 'textbox-question',
components: {
buttonMain
},
props: {
type: {
type: String,
default: 'text'
},
placeholderText: {
type: String,
default: ''
},
displayQuestionText: {
type: Boolean,
default: true
},
modelValue: String,
inputId: {
type: String,
required: true
},
isDisabled: Boolean,
isRequired: Boolean,
hasIcon: Boolean, // If input has an icon
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
hasError: Boolean,
mask: {
type: [String, Object],
default: ''
},
validationRules: String,
cmsWidgetName: {
type: String,
default: ''
},
maxLength: String,
questionAlignment: String, // Left or center. Left is default.
cornerStyle: String, // Rounded or square. Square is default.
includeSearchIcon: Boolean,
includeSelectIcon: Boolean,
min: String,
max: String,
disableAutoFill: Boolean,
buttonCallback: {
type: Function,
default: () => {}
}
},
emits: ['focus', 'update:modelValue', 'textboxQuestionEvent.inputIdAssigned', 'click-event'],
setup(props) {
const propsClone = { ...props };
const { modelValue } = propsClone;
let initialValue;
switch (typeof modelValue) {
case 'number':
initialValue = modelValue;
break;
default:
initialValue = modelValue && modelValue.length > 0 ? modelValue : '';
break;
}
const fieldOptions = {
type: 'text',
value: modelValue,
initialValue
};
const { errorMessage, handleBlur, handleChange, meta, validate, errors } =
useField(props.inputId, props.validationRules, fieldOptions);
return {
errorMessage,
handleBlur,
handleChange,
validate,
meta,
errors
};
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
},
questionTextForAria() {
return extractSpanText(this.getCmsContent(this.cmsWidgetName, 'QuestionText'));
},
buttonText() {
return this.getCmsContent(this.cmsWidgetName, 'ButtonText');
},
hasTextButton() {
return this.buttonText && this.buttonText.length > 0;
},
value: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit('update:modelValue', newValue);
}
},
getMaskFromProp() {
if (this.mask) {
if (this.mask.mask) {
return this.mask.mask;
}
return this.mask;
}
return null;
},
getTokensFromProp() {
if (this.mask?.tokens) {
return `{ '${this.mask.tokens.token}': { 'pattern': '${this.mask.tokens.pattern}' }}`;
}
return null;
}
},
watch: {
async value(newValue) {
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
if (result.valid) {
this.handleChange(newValue); // trigger full validation on this field only
}
}
},
mounted() {
this.$emit('textboxQuestionEvent.inputIdAssigned', this.inputId);
},
methods: {
trimOnPaste(evt) {
evt.stopPropagation();
evt.preventDefault();
const input = document.getElementById(evt.srcElement.id);
const originalValue = input.value;
const startPosition = input.selectionStart;
const endPosition = input.selectionEnd;
const selectionLength = endPosition - startPosition;
let data = null;
if (evt.type === 'paste') {
data = evt.clipboardData || window.clipboardData;
} else {
data = evt.dataTransfer;
}
const textValue = data.getData('Text');
// Insert the pasted/dropped string at the index position
const arrOriginalString = originalValue.split('');
arrOriginalString.splice(startPosition, selectionLength, textValue);
const blendedString = arrOriginalString.join('');
this.$emit('update:modelValue', blendedString);
},
clickedSearch() {
if (this.meta.valid) {
this.$emit('click-event');
}
},
async clickedTextButton() {
const result = await validate(this.value, this.validationRules);
if (result.valid) {
this.handleChange(this.value);
this.$emit('click-event');
}
}
}
};
</script>
<style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss";
input[type='date']::-webkit-inner-spin-button {
display: none;
}
input[type='date']::-webkit-calendar-picker-indicator {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 1px;
background-image: url($svg-calendar-picker);
background-repeat: no-repeat;
background-position: center;
width: 16px; // check this
height: 16px;
display: flex;
border-radius: 0 7px 7px 0;
background-color: #e4f1f7;
&:hover {
cursor: pointer;
}
padding: 15px 0;
min-width: 48px;
}
input::-webkit-date-and-time-value {
text-align: left !important;
}
.textbox-question {
label {
color: $black;
font-weight: 600;
}
span {
font-weight: 400;
font-size: 0.875rem;
color: #4d5151;
}
.form-test-error span {
color: $red;
font-size: 1rem;
font-weight: 400;
}
.input-wrapper {
position: relative;
&.has-search-icon {
display:flex;
.form-control {
&:focus, &:focus-within {
border: 1px solid #0070d1;
}
max-height: 3rem;
}
input[type='text'] {
border-color: #0070d1;
box-shadow: rgba(0, 0, 0, 0.2) 0rem 0.0625rem .25rem 0rem;
border-right: none;
border-radius: 4.5rem 0 0 4.5rem;
height: 3rem;
&:focus, &:focus-within {
border-right:none;
}
}
button[type='submit'] {
background-image: url($svg-search-icon);
background-repeat: no-repeat;
background-position: center;
border-radius: 0 4.5rem 4.5rem 0;
background-color: $blue-100;
width: 2.75rem;
border: 1px solid #0070d1;
border-left: none;
display: flex;
}
}
&.has-select-icon {
button[type='submit'] {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 1rem;
background-image: url($svg-select-icon);
background-repeat: no-repeat;
background-position: center;
background-color: transparent;
width: 1rem;
height: 100%;
border: 0px;
border-left: none;
display: flex;
}
}
&.has-text-button {
display: flex;
input[type='text'] {
border-radius: 1.5rem;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
min-width: 6.875rem;
}
}
}
input {
&.has-icon {
background-image: url(~@/assets/img/icons/location-pin.svg);
background-repeat: no-repeat;
background-position: 0.75rem 50%;
background-size: auto 1rem;
padding: 0 0.75rem 0 2.5rem;
&.icon-right {
background-position: calc(100% - 0.75rem) 50%;
padding: 0 2.5rem 0 0.75rem;
}
}
}
.form-label {
margin-bottom: 0.25rem;
}
.form-control {
border: $border-input;
border-radius: $border-radius;
min-height: 2.8125rem;
max-height: 2.8125rem;
padding: 0.75rem 1rem;
letter-spacing: inherit;
box-shadow: $box-shadow-input;
&::placeholder {
color: $gray-500;
}
&:focus, &:focus-within {
border: $border-input-focus;
}
&:disabled,
&.disabled {
background-color: $gray-100;
cursor: not-allowed;
}
}
p {
display: none;
}
}
</style>