You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
5.4 KiB
226 lines
5.4 KiB
/** * AmountInput.vue - Specialized amount input with increment/decrement
|
|
controls * * Extracted from GiftedDialog.vue to handle numeric amount input *
|
|
with increment/decrement buttons and validation. * * @author Matthew Raymer */
|
|
<template>
|
|
<div class="flex flex-grow">
|
|
<button
|
|
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
|
|
:disabled="isAtMinimum"
|
|
type="button"
|
|
@click.prevent="decrement"
|
|
>
|
|
<font-awesome icon="chevron-left" />
|
|
</button>
|
|
|
|
<input
|
|
:id="inputId"
|
|
v-model="displayValue"
|
|
type="number"
|
|
:min="min"
|
|
:max="max"
|
|
:step="step"
|
|
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
|
|
@input="handleInput"
|
|
@blur="handleBlur"
|
|
/>
|
|
|
|
<button
|
|
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
|
|
:disabled="isAtMaximum"
|
|
type="button"
|
|
@click.prevent="increment"
|
|
>
|
|
<font-awesome icon="chevron-right" />
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Prop, Vue, Watch, Emit } from "vue-facing-decorator";
|
|
import { logger } from "@/utils/logger";
|
|
|
|
/**
|
|
* AmountInput - Numeric input with increment/decrement controls
|
|
*
|
|
* Features:
|
|
* - Increment/decrement buttons with validation
|
|
* - Configurable min/max values and step size
|
|
* - Input validation and formatting
|
|
* - Disabled state handling for boundary values
|
|
* - Emits update events for v-model compatibility
|
|
*/
|
|
@Component
|
|
export default class AmountInput extends Vue {
|
|
/** Current numeric value */
|
|
@Prop({ required: true })
|
|
value!: number;
|
|
|
|
/** Minimum allowed value */
|
|
@Prop({ default: 0 })
|
|
min!: number;
|
|
|
|
/** Maximum allowed value */
|
|
@Prop({ default: Number.MAX_SAFE_INTEGER })
|
|
max!: number;
|
|
|
|
/** Step size for increment/decrement */
|
|
@Prop({ default: 1 })
|
|
step!: number;
|
|
|
|
/** Input element ID for accessibility */
|
|
@Prop({ default: "amount-input" })
|
|
inputId!: string;
|
|
|
|
/** Internal display value for input field */
|
|
private displayValue: string = "0";
|
|
|
|
/**
|
|
* Initialize display value from prop
|
|
*/
|
|
mounted(): void {
|
|
logger.debug("[AmountInput] mounted()", {
|
|
value: this.value,
|
|
min: this.min,
|
|
max: this.max,
|
|
step: this.step,
|
|
});
|
|
this.displayValue = this.value.toString();
|
|
logger.debug("[AmountInput] mounted() - displayValue set", {
|
|
displayValue: this.displayValue,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Watch for external value changes
|
|
*/
|
|
@Watch("value")
|
|
onValueChange(newValue: number): void {
|
|
this.displayValue = newValue.toString();
|
|
}
|
|
|
|
/**
|
|
* Check if current value is at minimum
|
|
*/
|
|
get isAtMinimum(): boolean {
|
|
const result = this.value <= this.min;
|
|
logger.debug("[AmountInput] isAtMinimum", {
|
|
value: this.value,
|
|
min: this.min,
|
|
result,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Check if current value is at maximum
|
|
*/
|
|
get isAtMaximum(): boolean {
|
|
const result = this.value >= this.max;
|
|
logger.debug("[AmountInput] isAtMaximum", {
|
|
value: this.value,
|
|
max: this.max,
|
|
result,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Increment the value by step size
|
|
*/
|
|
increment(): void {
|
|
logger.debug("[AmountInput] increment() called", {
|
|
currentValue: this.value,
|
|
step: this.step,
|
|
});
|
|
const newValue = Math.min(this.value + this.step, this.max);
|
|
logger.debug("[AmountInput] increment() calculated newValue", {
|
|
newValue,
|
|
});
|
|
this.updateValue(newValue);
|
|
}
|
|
|
|
/**
|
|
* Decrement the value by step size
|
|
*/
|
|
decrement(): void {
|
|
logger.debug("[AmountInput] decrement() called", {
|
|
currentValue: this.value,
|
|
step: this.step,
|
|
});
|
|
const newValue = Math.max(this.value - this.step, this.min);
|
|
logger.debug("[AmountInput] decrement() calculated newValue", {
|
|
newValue,
|
|
});
|
|
this.updateValue(newValue);
|
|
}
|
|
|
|
/**
|
|
* Handle direct input changes
|
|
*/
|
|
handleInput(): void {
|
|
const numericValue = parseFloat(this.displayValue);
|
|
if (!isNaN(numericValue)) {
|
|
const clampedValue = Math.max(this.min, Math.min(numericValue, this.max));
|
|
this.updateValue(clampedValue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle input blur - ensure display value matches actual value
|
|
*/
|
|
handleBlur(): void {
|
|
this.displayValue = this.value.toString();
|
|
}
|
|
|
|
/**
|
|
* Update the value and emit change event
|
|
*/
|
|
private updateValue(newValue: number): void {
|
|
logger.debug("[AmountInput] updateValue() called", {
|
|
oldValue: this.value,
|
|
newValue,
|
|
});
|
|
if (newValue !== this.value) {
|
|
logger.debug(
|
|
"[AmountInput] updateValue() - values different, updating and emitting",
|
|
);
|
|
this.displayValue = newValue.toString();
|
|
this.emitUpdateValue(newValue);
|
|
} else {
|
|
logger.debug(
|
|
"[AmountInput] updateValue() - values same, skipping update",
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit update:value event
|
|
*/
|
|
@Emit("update:value")
|
|
emitUpdateValue(value: number): number {
|
|
logger.debug("[AmountInput] emitUpdateValue() - emitting value", {
|
|
value,
|
|
});
|
|
return value;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Remove spinner arrows from number input */
|
|
input[type="number"]::-webkit-outer-spin-button,
|
|
input[type="number"]::-webkit-inner-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
|
|
input[type="number"] {
|
|
-moz-appearance: textfield;
|
|
}
|
|
|
|
/* Disabled button styles */
|
|
button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|
|
|