UNPKG

vxe-pc-ui

Version:
886 lines (885 loc) 34.8 kB
import { defineComponent, h, ref, computed, reactive, inject, nextTick, watch, onMounted, createCommentVNode, onBeforeUnmount } from 'vue'; import XEUtils from 'xe-utils'; import { getConfig, getIcon, getI18n, globalEvents, GLOBAL_EVENT_KEYS, createEvent, useSize } from '../../ui'; import { getFuncText, eqEmptyValue } from '../../ui/src/utils'; import { hasClass, getEventTargetNode } from '../../ui/src/dom'; import { getSlotVNs } from '../..//ui/src/vn'; import { handleNumber, toFloatValueFixed } from './util'; export default defineComponent({ name: 'VxeNumberInput', props: { modelValue: [String, Number], immediate: { type: Boolean, default: true }, name: String, type: { type: String, default: 'number' }, clearable: { type: Boolean, default: () => getConfig().numberInput.clearable }, readonly: { type: Boolean, default: null }, disabled: { type: Boolean, default: null }, placeholder: String, maxLength: { type: [String, Number], default: () => getConfig().numberInput.maxLength }, autoComplete: { type: String, default: 'off' }, align: String, form: String, className: String, size: { type: String, default: () => getConfig().numberInput.size || getConfig().size }, // number、integer、float min: { type: [String, Number], default: null }, max: { type: [String, Number], default: null }, step: [String, Number], exponential: { type: Boolean, default: () => getConfig().numberInput.exponential }, showCurrency: { type: Boolean, default: () => getConfig().numberInput.showCurrency }, currencySymbol: { type: String, default: () => getConfig().numberInput.currencySymbol }, // number、integer、float controls: { type: Boolean, default: () => getConfig().numberInput.controls }, // float digits: { type: [String, Number], default: null }, autoFill: { type: Boolean, default: () => getConfig().numberInput.autoFill }, editable: { type: Boolean, default: true }, prefixIcon: String, suffixIcon: String, // 已废弃 maxlength: [String, Number], // 已废弃 autocomplete: String }, emits: [ 'update:modelValue', 'input', 'change', 'keydown', 'keyup', 'wheel', 'click', 'focus', 'blur', 'clear', 'prev-number', 'next-number', 'prefix-click', 'suffix-click' ], setup(props, context) { const { slots, emit } = context; const $xeForm = inject('$xeForm', null); const formItemInfo = inject('xeFormItemInfo', null); const xID = XEUtils.uniqueId(); const { computeSize } = useSize(props); const reactData = reactive({ isFocus: false, isActivated: false, inputValue: props.modelValue }); const internalData = { // dnTimeout: undefined, // isUM: undefined }; const refElem = ref(); const refInputTarget = ref(); const refInputPanel = ref(); const refMaps = { refElem, refInput: refInputTarget }; const $xeNumberInput = { xID, props, context, reactData, internalData, getRefMaps: () => refMaps }; let numberInputMethods = {}; const computeFormReadonly = computed(() => { const { readonly } = props; if (readonly === null) { if ($xeForm) { return $xeForm.props.readonly; } return false; } return readonly; }); const computeIsDisabled = computed(() => { const { disabled } = props; if (disabled === null) { if ($xeForm) { return $xeForm.props.disabled; } return false; } return disabled; }); const computeDigitsValue = computed(() => { const { type, digits } = props; let defDigits = digits; if (defDigits === null) { defDigits = getConfig().numberInput.digits; if (defDigits === null) { if (type === 'amount') { defDigits = 2; } } } return XEUtils.toInteger(defDigits) || 1; }); const computeDecimalsType = computed(() => { const { type } = props; return type === 'float' || type === 'amount'; }); const computeStepValue = computed(() => { const { type } = props; const digitsValue = computeDigitsValue.value; const decimalsType = computeDecimalsType.value; const step = props.step; if (type === 'integer') { return XEUtils.toInteger(step) || 1; } else if (decimalsType) { return XEUtils.toNumber(step) || (1 / Math.pow(10, digitsValue)); } return XEUtils.toNumber(step) || 1; }); const computeIsClearable = computed(() => { return props.clearable; }); const computeInputReadonly = computed(() => { const { editable } = props; const formReadonly = computeFormReadonly.value; return formReadonly || !editable; }); const computeInpPlaceholder = computed(() => { const { placeholder } = props; if (placeholder) { return getFuncText(placeholder); } const globalPlaceholder = getConfig().numberInput.placeholder; if (globalPlaceholder) { return getFuncText(globalPlaceholder); } return getI18n('vxe.base.pleaseInput'); }); const computeInpMaxLength = computed(() => { const { maxLength, maxlength } = props; // 数值最大长度限制 16 位,包含小数 return XEUtils.toNumber(maxLength || maxlength) || 16; }); const computeInpImmediate = computed(() => { const { immediate } = props; return immediate; }); const computeNumValue = computed(() => { const { type } = props; const { inputValue } = reactData; return type === 'integer' ? XEUtils.toInteger(handleNumber(inputValue)) : XEUtils.toNumber(handleNumber(inputValue)); }); const computeNumLabel = computed(() => { const { type, showCurrency, currencySymbol, autoFill } = props; const { inputValue } = reactData; const digitsValue = computeDigitsValue.value; if (type === 'amount') { const num = XEUtils.toNumber(inputValue); let amountLabel = XEUtils.commafy(num, { digits: digitsValue }); if (!autoFill) { const [iStr, dStr] = amountLabel.split('.'); if (dStr) { const dRest = dStr.replace(/0+$/, ''); amountLabel = dRest ? [iStr, '.', dRest].join('') : iStr; } } if (showCurrency) { return `${currencySymbol || getI18n('vxe.numberInput.currencySymbol') || ''}${amountLabel}`; } return amountLabel; } return XEUtils.toString(inputValue); }); const computeIsDisabledSubtractNumber = computed(() => { const { min } = props; const { inputValue } = reactData; const numValue = computeNumValue.value; // 当有值时再进行判断 if ((inputValue || inputValue === 0) && min !== null) { return numValue <= XEUtils.toNumber(min); } return false; }); const computeIsDisabledAddNumber = computed(() => { const { max } = props; const { inputValue } = reactData; const numValue = computeNumValue.value; // 当有值时再进行判断 if ((inputValue || inputValue === 0) && max !== null) { return numValue >= XEUtils.toNumber(max); } return false; }); const handleNumberString = (val) => { if (XEUtils.eqNull(val)) { return ''; } return `${val}`; }; const getNumberValue = (val) => { const { exponential, autoFill } = props; const inpMaxLength = computeInpMaxLength.value; const digitsValue = computeDigitsValue.value; const decimalsType = computeDecimalsType.value; let restVal = ''; if (decimalsType) { restVal = toFloatValueFixed(val, digitsValue); if (!autoFill) { restVal = handleNumberString(XEUtils.toNumber(restVal)); } } else { restVal = handleNumberString(val); } if (exponential && (val === restVal || handleNumberString(val).toLowerCase() === XEUtils.toNumber(restVal).toExponential())) { return val; } return restVal.slice(0, inpMaxLength); }; const triggerEvent = (evnt) => { const { inputValue } = reactData; numberInputMethods.dispatchEvent(evnt.type, { value: inputValue }, evnt); }; const handleChange = (val, inputValue, evnt) => { const value = eqEmptyValue(val) ? null : Number(val); const isChange = value !== props.modelValue; if (isChange) { internalData.isUM = true; emit('update:modelValue', value); } if (reactData.inputValue !== inputValue) { nextTick(() => { reactData.inputValue = inputValue || ''; }); } numberInputMethods.dispatchEvent('input', { value }, evnt); if (isChange) { numberInputMethods.dispatchEvent('change', { value }, evnt); // 自动更新校验状态 if ($xeForm && formItemInfo) { $xeForm.triggerItemEvent(evnt, formItemInfo.itemConfig.field, value); } } }; const emitInputEvent = (inputValue, evnt) => { const inpImmediate = computeInpImmediate.value; const value = eqEmptyValue(inputValue) ? null : XEUtils.toNumber(inputValue); reactData.inputValue = inputValue; if (inpImmediate) { handleChange(value, inputValue, evnt); } else { numberInputMethods.dispatchEvent('input', { value }, evnt); } }; const inputEvent = (evnt) => { const inputElem = evnt.target; const value = inputElem.value; emitInputEvent(value, evnt); }; const changeEvent = (evnt) => { const inpImmediate = computeInpImmediate.value; if (!inpImmediate) { triggerEvent(evnt); } }; const focusEvent = (evnt) => { const inputReadonly = computeInputReadonly.value; if (!inputReadonly) { const { inputValue } = reactData; reactData.inputValue = eqEmptyValue(inputValue) ? '' : `${XEUtils.toNumber(inputValue)}`; reactData.isFocus = true; reactData.isActivated = true; triggerEvent(evnt); } }; const clickPrefixEvent = (evnt) => { const isDisabled = computeIsDisabled.value; if (!isDisabled) { const { inputValue } = reactData; numberInputMethods.dispatchEvent('prefix-click', { value: inputValue }, evnt); } }; const clearValueEvent = (evnt, value) => { focus(); handleChange(null, '', evnt); numberInputMethods.dispatchEvent('clear', { value }, evnt); }; const clickSuffixEvent = (evnt) => { const isDisabled = computeIsDisabled.value; if (!isDisabled) { const { inputValue } = reactData; numberInputMethods.dispatchEvent('suffix-click', { value: inputValue }, evnt); } }; const updateModel = (val) => { const { autoFill } = props; const { inputValue } = reactData; const digitsValue = computeDigitsValue.value; const decimalsType = computeDecimalsType.value; if (eqEmptyValue(val)) { reactData.inputValue = ''; } else { let textValue = `${val}`; if (decimalsType) { textValue = toFloatValueFixed(val, digitsValue); if (!autoFill) { textValue = `${XEUtils.toNumber(textValue)}`; } } if (textValue !== inputValue) { reactData.inputValue = textValue; } } }; /** * 检查初始值 */ const initValue = () => { const { autoFill } = props; const { inputValue } = reactData; const digitsValue = computeDigitsValue.value; const decimalsType = computeDecimalsType.value; if (decimalsType) { if (inputValue) { let textValue = ''; let validValue = null; if (inputValue) { textValue = toFloatValueFixed(inputValue, digitsValue); validValue = XEUtils.toNumber(textValue); if (!autoFill) { textValue = `${validValue}`; } } if (inputValue !== validValue) { handleChange(validValue, textValue, { type: 'init' }); } else { reactData.inputValue = textValue; } } } }; const validMaxNum = (num) => { return props.max === null || XEUtils.toNumber(num) <= XEUtils.toNumber(props.max); }; const validMinNum = (num) => { return props.min === null || XEUtils.toNumber(num) >= XEUtils.toNumber(props.min); }; const afterCheckValue = () => { const { type, min, max, exponential } = props; const { inputValue } = reactData; const inputReadonly = computeInputReadonly.value; if (!inputReadonly) { if (eqEmptyValue(inputValue)) { let inpNumVal = null; let inpValue = inputValue; if (min || min === 0) { inpNumVal = XEUtils.toNumber(min); inpValue = `${inpNumVal}`; } handleChange(inpNumVal, `${inpValue || ''}`, { type: 'check' }); return; } if (inputValue || (min || max)) { let inpNumVal = type === 'integer' ? XEUtils.toInteger(handleNumber(inputValue)) : XEUtils.toNumber(handleNumber(inputValue)); if (!validMinNum(inpNumVal)) { inpNumVal = min; } else if (!validMaxNum(inpNumVal)) { inpNumVal = max; } if (exponential) { const inpStringVal = handleNumberString(inputValue).toLowerCase(); if (inpStringVal === XEUtils.toNumber(inpNumVal).toExponential()) { inpNumVal = inpStringVal; } } const inpValue = getNumberValue(inpNumVal); handleChange(eqEmptyValue(inpValue) ? null : Number(inpValue), inpValue, { type: 'check' }); } } }; const blurEvent = (evnt) => { const { inputValue } = reactData; const inpImmediate = computeInpImmediate.value; const value = inputValue ? Number(inputValue) : null; if (!inpImmediate) { handleChange(value, handleNumberString(inputValue), evnt); } afterCheckValue(); reactData.isFocus = false; reactData.isActivated = false; numberInputMethods.dispatchEvent('blur', { value }, evnt); // 自动更新校验状态 if ($xeForm && formItemInfo) { $xeForm.triggerItemEvent(evnt, formItemInfo.itemConfig.field, value); } }; // 数值 const numberChange = (isPlus, evnt) => { const { min, max, type } = props; const { inputValue } = reactData; const stepValue = computeStepValue.value; const numValue = type === 'integer' ? XEUtils.toInteger(handleNumber(inputValue)) : XEUtils.toNumber(handleNumber(inputValue)); const newValue = isPlus ? XEUtils.add(numValue, stepValue) : XEUtils.subtract(numValue, stepValue); let restNum; if (!validMinNum(newValue)) { restNum = min; } else if (!validMaxNum(newValue)) { restNum = max; } else { restNum = newValue; } emitInputEvent(getNumberValue(restNum), evnt); }; const numberNextEvent = (evnt) => { const isDisabled = computeIsDisabled.value; const formReadonly = computeFormReadonly.value; const isDisabledSubtractNumber = computeIsDisabledSubtractNumber.value; numberStopDown(); if (!isDisabled && !formReadonly && !isDisabledSubtractNumber) { numberChange(false, evnt); } reactData.isActivated = true; numberInputMethods.dispatchEvent('next-number', { value: reactData.inputValue }, evnt); }; const numberDownNextEvent = (evnt) => { internalData.dnTimeout = setTimeout(() => { numberNextEvent(evnt); numberDownNextEvent(evnt); }, 60); }; const numberPrevEvent = (evnt) => { const isDisabled = computeIsDisabled.value; const formReadonly = computeFormReadonly.value; const isDisabledAddNumber = computeIsDisabledAddNumber.value; numberStopDown(); if (!isDisabled && !formReadonly && !isDisabledAddNumber) { numberChange(true, evnt); } reactData.isActivated = true; numberInputMethods.dispatchEvent('prev-number', { value: reactData.inputValue }, evnt); }; const numberKeydownEvent = (evnt) => { const isUpArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_UP); const isDwArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_DOWN); if (isUpArrow || isDwArrow) { evnt.preventDefault(); if (isUpArrow) { numberPrevEvent(evnt); } else { numberNextEvent(evnt); } } }; const keydownEvent = (evnt) => { const { exponential, controls } = props; const inputReadonly = computeInputReadonly.value; const isCtrlKey = evnt.ctrlKey; const isShiftKey = evnt.shiftKey; const isAltKey = evnt.altKey; const isMetaKey = evnt.metaKey; const keyCode = evnt.keyCode; const isEsc = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ESCAPE); const isUpArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_UP); const isDwArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_DOWN); if (!isCtrlKey && !isShiftKey && !isAltKey && !isMetaKey && (globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.SPACEBAR) || ((!exponential || keyCode !== 69) && (keyCode >= 65 && keyCode <= 90)) || (keyCode >= 186 && keyCode <= 188) || keyCode >= 191)) { evnt.preventDefault(); } if (isEsc) { afterCheckValue(); } else if (isUpArrow || isDwArrow) { if (controls && !inputReadonly) { numberKeydownEvent(evnt); } } triggerEvent(evnt); }; const keyupEvent = (evnt) => { triggerEvent(evnt); }; // 数值 const numberStopDown = () => { const { dnTimeout } = internalData; if (dnTimeout) { clearTimeout(dnTimeout); internalData.dnTimeout = undefined; } }; const numberDownPrevEvent = (evnt) => { internalData.dnTimeout = setTimeout(() => { numberPrevEvent(evnt); numberDownPrevEvent(evnt); }, 60); }; const numberMousedownEvent = (evnt) => { numberStopDown(); if (evnt.button === 0) { const isPrevNumber = hasClass(evnt.currentTarget, 'is--prev'); if (isPrevNumber) { numberPrevEvent(evnt); } else { numberNextEvent(evnt); } internalData.dnTimeout = setTimeout(() => { if (isPrevNumber) { numberDownPrevEvent(evnt); } else { numberDownNextEvent(evnt); } }, 500); } }; const wheelEvent = (evnt) => { const inputReadonly = computeInputReadonly.value; if (props.controls && !inputReadonly) { if (reactData.isActivated) { evnt.stopPropagation(); evnt.preventDefault(); const delta = evnt.deltaY; if (delta > 0) { numberNextEvent(evnt); } else if (delta < 0) { numberPrevEvent(evnt); } } } triggerEvent(evnt); }; const clickEvent = (evnt) => { triggerEvent(evnt); }; // 全局事件 const handleGlobalMousedownEvent = (evnt) => { const { isActivated } = reactData; const el = refElem.value; const panelElem = refInputPanel.value; const isDisabled = computeIsDisabled.value; const inputReadonly = computeInputReadonly.value; const inpImmediate = computeInpImmediate.value; if (!isDisabled && !inputReadonly && isActivated) { reactData.isActivated = getEventTargetNode(evnt, el).flag || getEventTargetNode(evnt, panelElem).flag; if (!reactData.isActivated) { if (!inpImmediate) { const { inputValue } = reactData; const value = inputValue ? Number(inputValue) : null; handleChange(value, handleNumberString(inputValue), evnt); } afterCheckValue(); } } }; const handleGlobalKeydownEvent = (evnt) => { const { clearable } = props; const isDisabled = computeIsDisabled.value; const inputReadonly = computeInputReadonly.value; if (!isDisabled && !inputReadonly) { const isTab = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.TAB); const isDel = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.DELETE); let isActivated = reactData.isActivated; if (isTab) { if (isActivated) { afterCheckValue(); } isActivated = false; reactData.isActivated = isActivated; } if (isDel && clearable) { if (isActivated) { clearValueEvent(evnt, null); } } } }; const handleGlobalBlurEvent = () => { const { isActivated } = reactData; if (isActivated) { afterCheckValue(); } }; const renderNumberIcon = () => { const isDisabledAddNumber = computeIsDisabledAddNumber.value; const isDisabledSubtractNumber = computeIsDisabledSubtractNumber.value; return h('div', { class: 'vxe-input--control-icon' }, [ h('div', { class: 'vxe-input--number-icon' }, [ h('div', { class: ['vxe-input--number-btn is--prev', { 'is--disabled': isDisabledAddNumber }], onMousedown: numberMousedownEvent, onMouseup: numberStopDown, onMouseleave: numberStopDown }, [ h('i', { class: getIcon().NUMBER_INPUT_PREV_NUM }) ]), h('div', { class: ['vxe-input--number-btn is--next', { 'is--disabled': isDisabledSubtractNumber }], onMousedown: numberMousedownEvent, onMouseup: numberStopDown, onMouseleave: numberStopDown }, [ h('i', { class: getIcon().NUMBER_INPUT_NEXT_NUM }) ]) ]) ]); }; const renderPrefixIcon = () => { const { prefixIcon } = props; const prefixSlot = slots.prefix; return prefixSlot || prefixIcon ? h('div', { class: 'vxe-number-input--prefix', onClick: clickPrefixEvent }, [ h('div', { class: 'vxe-number-input--prefix-icon' }, prefixSlot ? getSlotVNs(prefixSlot({})) : [ h('i', { class: prefixIcon }) ]) ]) : null; }; const renderSuffixIcon = () => { const { suffixIcon } = props; const { inputValue } = reactData; const suffixSlot = slots.suffix; const isDisabled = computeIsDisabled.value; const isClearable = computeIsClearable.value; return h('div', { class: ['vxe-number-input--suffix', { 'is--clear': isClearable && !isDisabled && !(inputValue === '' || XEUtils.eqNull(inputValue)) }] }, [ isClearable ? h('div', { class: 'vxe-number-input--clear-icon', onClick: clearValueEvent }, [ h('i', { class: getIcon().INPUT_CLEAR }) ]) : createCommentVNode(), renderExtraSuffixIcon(), suffixSlot || suffixIcon ? h('div', { class: 'vxe-number-input--suffix-icon', onClick: clickSuffixEvent }, suffixSlot ? getSlotVNs(suffixSlot({})) : [ h('i', { class: suffixIcon }) ]) : createCommentVNode() ]); }; const renderExtraSuffixIcon = () => { const { controls } = props; const inputReadonly = computeInputReadonly.value; if (controls && !inputReadonly) { return renderNumberIcon(); } return createCommentVNode(); }; const dispatchEvent = (type, params, evnt) => { emit(type, createEvent(evnt, { $numberInput: $xeNumberInput }, params)); }; numberInputMethods = { dispatchEvent, focus() { const inputReadonly = computeInputReadonly.value; if (!inputReadonly) { const inputElem = refInputTarget.value; reactData.isActivated = true; inputElem.focus(); } return nextTick(); }, blur() { const inputElem = refInputTarget.value; inputElem.blur(); reactData.isActivated = false; return nextTick(); }, select() { const inputElem = refInputTarget.value; inputElem.select(); reactData.isActivated = false; return nextTick(); } }; Object.assign($xeNumberInput, numberInputMethods); const renderVN = () => { const { className, controls, type, align, name, autocomplete, autoComplete } = props; const { inputValue, isFocus, isActivated } = reactData; const vSize = computeSize.value; const isDisabled = computeIsDisabled.value; const formReadonly = computeFormReadonly.value; const numLabel = computeNumLabel.value; if (formReadonly) { return h('div', { ref: refElem, class: ['vxe-number-input--readonly', `type--${type}`, className] }, numLabel); } const inputReadonly = computeInputReadonly.value; const inpMaxLength = computeInpMaxLength.value; const inpPlaceholder = computeInpPlaceholder.value; const isClearable = computeIsClearable.value; const prefix = renderPrefixIcon(); const suffix = renderSuffixIcon(); return h('div', { ref: refElem, class: ['vxe-number-input', `type--${type}`, className, { [`size--${vSize}`]: vSize, [`is--${align}`]: align, 'is--controls': controls && !inputReadonly, 'is--prefix': !!prefix, 'is--suffix': !!suffix, 'is--disabled': isDisabled, 'is--active': isActivated, 'show--clear': isClearable && !isDisabled && !(inputValue === '' || XEUtils.eqNull(inputValue)) }], spellcheck: false }, [ prefix || createCommentVNode(), h('div', { class: 'vxe-number-input--wrapper' }, [ h('input', { ref: refInputTarget, class: 'vxe-number-input--inner', value: !isFocus && type === 'amount' ? numLabel : inputValue, name, type: 'text', placeholder: inpPlaceholder, maxlength: inpMaxLength, readonly: inputReadonly, disabled: isDisabled, autocomplete: autoComplete || autocomplete, onKeydown: keydownEvent, onKeyup: keyupEvent, onWheel: wheelEvent, onClick: clickEvent, onInput: inputEvent, onChange: changeEvent, onFocus: focusEvent, onBlur: blurEvent }) ]), suffix || createCommentVNode() ]); }; $xeNumberInput.renderVN = renderVN; watch(() => props.modelValue, (val) => { if (!internalData.isUM) { updateModel(val); } internalData.isUM = false; }); watch(() => props.type, () => { // 切换类型是重置内置变量 Object.assign(reactData, { inputValue: props.modelValue }); initValue(); }); onMounted(() => { globalEvents.on($xeNumberInput, 'mousedown', handleGlobalMousedownEvent); globalEvents.on($xeNumberInput, 'keydown', handleGlobalKeydownEvent); globalEvents.on($xeNumberInput, 'blur', handleGlobalBlurEvent); }); onBeforeUnmount(() => { reactData.isFocus = false; numberStopDown(); afterCheckValue(); globalEvents.off($xeNumberInput, 'mousedown'); globalEvents.off($xeNumberInput, 'keydown'); globalEvents.off($xeNumberInput, 'blur'); }); initValue(); return $xeNumberInput; }, render() { return this.renderVN(); } });