Comparing version 0.0.766 to 0.0.767
{ | ||
"name": "vueless", | ||
"version": "0.0.766", | ||
"version": "0.0.767", | ||
"license": "MIT", | ||
@@ -5,0 +5,0 @@ "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.", |
@@ -20,3 +20,3 @@ import defaultConfig from "./config.ts"; | ||
*/ | ||
modelValue?: number | string; | ||
modelValue?: string; | ||
/** | ||
@@ -23,0 +23,0 @@ * Input label. |
@@ -1,2 +0,2 @@ | ||
import { onMounted, nextTick, ref, onBeforeUnmount, toValue, watch } from "vue"; | ||
import { onMounted, nextTick, ref, onBeforeUnmount, toValue, watch, computed, readonly } from "vue"; | ||
@@ -7,7 +7,12 @@ import { getRawValue, getFormattedValue } from "./utilFormat.ts"; | ||
const digitSet = ["1", "2", "3", "4", "5", "6", "7", "9", "0"]; | ||
const rawDecimalMark = "."; | ||
const comma = ","; | ||
const arrowKeys = ["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"]; | ||
const minus = "-"; | ||
export default function useFormatCurrency( | ||
elementId: string = "", | ||
options: (() => FormatOptions) | FormatOptions, | ||
formatOptions: (() => FormatOptions) | FormatOptions, | ||
) { | ||
let prevValue = ""; | ||
let inputElement: HTMLInputElement | null = null; | ||
@@ -17,6 +22,8 @@ | ||
const rawValue = ref(""); | ||
const prevValue = ref(""); | ||
// update value according to updated options | ||
const options = computed(() => toValue(formatOptions)); | ||
watch( | ||
() => toValue(options), | ||
() => options, | ||
() => setValue(formattedValue.value), | ||
@@ -31,3 +38,3 @@ { deep: true }, | ||
inputElement.addEventListener("input", onInput); | ||
onInput(formattedValue.value as unknown as InputEvent); | ||
inputElement.addEventListener("keydown", onKeydown); | ||
} | ||
@@ -42,59 +49,194 @@ }); | ||
// Use to set input value manually | ||
function setValue(value: string | number) { | ||
const localFormattedValue = getFormattedValue(value, toValue(options)); | ||
/** | ||
* Set input value manually. | ||
* @param {Intl.StringNumericLiteral} value | ||
* @returns {void} | ||
*/ | ||
function setValue(value: string) { | ||
const newFormattedValue = getFormattedValue(value, options.value); | ||
formattedValue.value = localFormattedValue; | ||
rawValue.value = getRawValue(localFormattedValue, toValue(options)); | ||
formattedValue.value = newFormattedValue; | ||
rawValue.value = getRawValue(newFormattedValue, options.value); | ||
prevValue = formattedValue.value; | ||
prevValue.value = formattedValue.value; | ||
} | ||
function onKeydown(event: KeyboardEvent) { | ||
if (!event.target || !inputElement) return; | ||
const cursorStart = inputElement.selectionStart || 0; | ||
const cursorEnd = inputElement.selectionEnd || 0; | ||
const isEndOfValue = cursorEnd === formattedValue.value.length; | ||
const isKeyCombination = event.ctrlKey || event.shiftKey || event.metaKey || event.altKey; | ||
const isSelection = cursorEnd !== cursorStart; | ||
if (event.key === "Backspace" && !isSelection) { | ||
const charToRemove = inputElement.value[cursorStart - 1]; | ||
const isFormatChar = [ | ||
options.value.thousandsSeparator, | ||
options.value.prefix, | ||
options.value.decimalSeparator, | ||
].includes(charToRemove); | ||
// Skip unremovable character and put cursor one step back. | ||
if (isFormatChar && !inputElement.value.endsWith(options.value.decimalSeparator)) { | ||
event.preventDefault(); | ||
inputElement.setSelectionRange(cursorStart - 1, cursorEnd - 1); | ||
} | ||
return; | ||
} | ||
const endsWithDecimal = formattedValue.value.endsWith(options.value.decimalSeparator); | ||
const includesDecimalMark = | ||
formattedValue.value.includes(options.value.decimalSeparator) && !endsWithDecimal; | ||
const isCharKey = !arrowKeys.includes(event.key) && !isKeyCombination; | ||
if ((event.key === comma || event.key === rawDecimalMark) && endsWithDecimal) { | ||
event.preventDefault(); | ||
return; | ||
} | ||
if (isEndOfValue && includesDecimalMark && isCharKey && !isSelection) { | ||
const fraction = prevValue.value.split(options.value.decimalSeparator).at(-1) || ""; | ||
if (fraction.length >= options.value.maxFractionDigits) { | ||
event.preventDefault(); | ||
} | ||
return; | ||
} | ||
} | ||
async function onInput(event: Event) { | ||
if (!event.target) return; | ||
if (!event.target || !inputElement) return; | ||
await nextTick(async () => { | ||
if (!inputElement) return; | ||
await nextTick(); | ||
let cursorStart = inputElement.selectionStart; | ||
let cursorEnd = inputElement.selectionEnd; | ||
const cursorStart = inputElement.selectionStart || 0; | ||
const cursorEnd = inputElement.selectionEnd || 0; | ||
const hasValueInputValue = cursorEnd === 1 && cursorStart === 1; | ||
const input = event.target as HTMLInputElement; | ||
const value = input.value || ""; | ||
const input = event.target as HTMLInputElement; | ||
const localFormattedValue = getFormattedValue(value, toValue(options)); | ||
let value = input.value || ""; | ||
const currentValueOffsetLength = localFormattedValue | ||
.split("") | ||
.filter((value: string) => value === toValue(options).thousandsSeparator).length; | ||
const prevCursorPosition = cursorEnd - 1; | ||
const eventData = (event as InputEvent).data || ""; | ||
const prevValueOffsetLength = prevValue | ||
.split("") | ||
.filter((value) => value === toValue(options).thousandsSeparator).length; | ||
if (value === minus) { | ||
formattedValue.value = minus; | ||
rawValue.value = minus; | ||
} | ||
const prefixLength = toValue(options).prefix.length; | ||
const offset = currentValueOffsetLength - prevValueOffsetLength; | ||
if (!value || value.startsWith(`${options.value.decimalSeparator}0`)) { | ||
formattedValue.value = options.value.prefix; | ||
rawValue.value = ""; | ||
formattedValue.value = localFormattedValue || toValue(options).prefix; | ||
rawValue.value = getRawValue(localFormattedValue, toValue(options)); | ||
return; | ||
} | ||
await nextTick(() => { | ||
if (localFormattedValue.length === cursorEnd || !cursorStart || !cursorEnd) return; | ||
// Replace dot with decimal separator | ||
if (eventData === rawDecimalMark || eventData === comma) { | ||
value = [ | ||
...prevValue.value.slice(0, prevCursorPosition), | ||
options.value.decimalSeparator, | ||
...prevValue.value.slice(prevCursorPosition), | ||
].join(""); | ||
} | ||
if (hasValueInputValue && prefixLength) { | ||
cursorStart += prefixLength; | ||
cursorEnd += prefixLength; | ||
} | ||
if (value.split(options.value.decimalSeparator).length > 2) { | ||
value = value.split("").with(value.lastIndexOf(options.value.decimalSeparator), "").join(""); | ||
} | ||
if (inputElement) { | ||
inputElement.setSelectionRange(cursorStart + offset, cursorEnd + offset); | ||
} | ||
}); | ||
if (value.endsWith(options.value.decimalSeparator)) { | ||
formattedValue.value = value; | ||
prevValue = formattedValue.value; | ||
}); | ||
return; | ||
} | ||
const newRawValue = getRawValue(value, options.value); | ||
const isNumericValue = eventData && digitSet.includes(eventData); | ||
const isMinus = cursorEnd === 1 && cursorStart === 1 && eventData === minus; | ||
const isDoubleMinus = isMinus && prevValue.value.startsWith(minus); | ||
const isMinusWithin = newRawValue.includes(minus) && !newRawValue.startsWith(minus); | ||
const isReservedSymbol = eventData !== rawDecimalMark && eventData !== comma; | ||
if ( | ||
(!isNumericValue && isReservedSymbol && !isMinus && eventData.length === 1) || | ||
isDoubleMinus || | ||
isMinusWithin | ||
) { | ||
inputElement.value = formattedValue.value; | ||
await nextTick(); | ||
inputElement.setSelectionRange(cursorStart, cursorEnd); | ||
return; | ||
} | ||
const newFormattedValue = getFormattedValue(newRawValue, options.value); | ||
if (Number.isNaN(newFormattedValue) || newFormattedValue.includes("NaN")) { | ||
inputElement.value = prevValue.value; | ||
return; | ||
} | ||
formattedValue.value = newFormattedValue; | ||
rawValue.value = getRawValue(newFormattedValue, options.value); | ||
inputElement.value = formattedValue.value; | ||
await setInputCursor(newFormattedValue, inputElement, cursorStart, cursorEnd); | ||
prevValue.value = formattedValue.value; | ||
} | ||
return { rawValue, formattedValue, setValue }; | ||
async function setInputCursor( | ||
newValue: string, | ||
inputElement: HTMLInputElement, | ||
prevCursorStart: number | null, | ||
prevCursorEnd: number | null, | ||
) { | ||
const hasValueInputValue = prevCursorStart === 1 && prevCursorEnd === 1; | ||
const currentValueOffsetLength = newValue | ||
.split("") | ||
.filter((value: string) => value === options.value.thousandsSeparator).length; | ||
const prevValueOffsetLength = prevValue.value | ||
.split("") | ||
.filter((value) => value === options.value.thousandsSeparator).length; | ||
const prefixLength = options.value.prefix.length; | ||
const offset = currentValueOffsetLength - prevValueOffsetLength; | ||
await nextTick(); | ||
if (offset < 0 && inputElement) { | ||
inputElement.setSelectionRange(prevCursorStart, prevCursorEnd); | ||
return; | ||
} | ||
if (newValue.length === prevCursorEnd || !prevCursorStart || !prevCursorEnd) return; | ||
let newCursorStart = prevCursorStart; | ||
let newCursorEnd = prevCursorEnd; | ||
if (hasValueInputValue && prefixLength) { | ||
newCursorStart += prefixLength; | ||
newCursorEnd += prefixLength; | ||
} | ||
if (inputElement) { | ||
inputElement.setSelectionRange(newCursorStart + offset, newCursorEnd + offset); | ||
} | ||
} | ||
return { rawValue: readonly(rawValue), formattedValue: readonly(formattedValue), setValue }; | ||
} |
import type { FormatOptions } from "./types.ts"; | ||
const isNumberValueRegExp = /^[\d,.\s-]+$/; | ||
const rawDecimalMark = "."; | ||
const minus = "-"; | ||
export function getRawValue(value: string | number, options: FormatOptions): string { | ||
export function getRawValue( | ||
value: string, | ||
options: Pick<FormatOptions, "prefix" | "decimalSeparator" | "thousandsSeparator">, | ||
): Intl.StringNumericLiteral { | ||
const { thousandsSeparator, decimalSeparator, prefix } = options; | ||
value = String(value).endsWith(decimalSeparator) | ||
? String(value).replace(decimalSeparator, "") | ||
: String(value); | ||
value = value.endsWith(decimalSeparator) ? value.replace(decimalSeparator, "") : value; | ||
const rawValueWithPrefix = value | ||
.replaceAll(thousandsSeparator, "") | ||
.replaceAll(" ", "") | ||
.replace(decimalSeparator, rawDecimalMark); | ||
return rawValueWithPrefix.replace(prefix, ""); | ||
return rawValueWithPrefix.replace(prefix, "") as Intl.StringNumericLiteral; | ||
} | ||
export function getFormattedValue(value: string | number, options: FormatOptions): string { | ||
const { | ||
thousandsSeparator, | ||
decimalSeparator, | ||
minFractionDigits, | ||
maxFractionDigits, | ||
prefix, | ||
positiveOnly, | ||
} = options; | ||
export function getFormattedValue(value: string, options: FormatOptions): string { | ||
const { thousandsSeparator, decimalSeparator, prefix, positiveOnly } = options; | ||
const invalidValuesRegExp = new RegExp("[^\\d,\\d.\\s-" + decimalSeparator + "]", "g"); | ||
const doubleValueRegExp = new RegExp("([,\\.\\s\\-" + decimalSeparator + "])+", "g"); | ||
const minFractionDigits = Math.abs(options.minFractionDigits); | ||
const maxFractionDigits = Math.abs(options.maxFractionDigits); | ||
const actualMinFractionDigit = | ||
minFractionDigits <= maxFractionDigits ? minFractionDigits : maxFractionDigits; | ||
const isValidMinFractionDigits = minFractionDigits <= maxFractionDigits; | ||
const actualMinFractionDigit = isValidMinFractionDigits ? minFractionDigits : maxFractionDigits; | ||
// slice to first decimal mark | ||
value = String(value) | ||
.replaceAll(rawDecimalMark, decimalSeparator) | ||
.split(decimalSeparator) | ||
.slice(0, 2) | ||
.map((value: string, index: number) => | ||
index ? value.replaceAll(thousandsSeparator, "") : value, | ||
) | ||
.join(decimalSeparator); | ||
value = String(value) | ||
.replace(invalidValuesRegExp, "") | ||
.replace(doubleValueRegExp, "$1") | ||
.replaceAll(decimalSeparator, rawDecimalMark) | ||
.trim(); | ||
const isNumber = isNumberValueRegExp.test(value); | ||
const isFloat = value.endsWith(rawDecimalMark) || value.endsWith(".0"); | ||
const isMinus = value === minus; | ||
if (isMinus && positiveOnly) { | ||
value = ""; | ||
} | ||
if (value.includes(minus)) { | ||
let isFirstMinus = value.startsWith(minus); | ||
value = value.replaceAll(minus, (match) => { | ||
if (isFirstMinus) { | ||
isFirstMinus = false; | ||
return match; | ||
} | ||
return ""; | ||
}); | ||
} | ||
if (!value || !isNumber || isFloat || isMinus) { | ||
return `${prefix}${value.replaceAll(rawDecimalMark, decimalSeparator)}`; | ||
} | ||
const intlNumberOptions: Intl.NumberFormatOptions = { | ||
@@ -92,13 +41,4 @@ minimumFractionDigits: actualMinFractionDigit, | ||
const rawValue = getRawValue(value, { | ||
decimalSeparator, | ||
thousandsSeparator, | ||
prefix, | ||
minFractionDigits: 0, | ||
maxFractionDigits: 2, | ||
positiveOnly: false, | ||
}); | ||
const formattedValue = intlNumber | ||
.formatToParts((rawValue || 0) as unknown as number) | ||
.formatToParts(value as Intl.StringNumericLiteral) | ||
.map((part) => { | ||
@@ -108,6 +48,2 @@ if (part.type === "group") part.value = thousandsSeparator; | ||
if (part.type === "fraction") { | ||
part.value = part.value.padEnd(maxFractionDigits, "0"); | ||
} | ||
return part; | ||
@@ -114,0 +50,0 @@ }); |
Sorry, the diff of this file is not supported yet
1042984
21705