UNPKG

quasar

Version:

Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time

499 lines (412 loc) 12.8 kB
import { h, ref, computed, watch, onBeforeUnmount, onMounted, nextTick, getCurrentInstance } from 'vue' import useField, { useFieldState, useFieldProps, useFieldEmits, fieldValueIsFilled } from '../../composables/private.use-field/use-field.js' import useMask, { useMaskProps } from './use-mask.js' import { useFormProps, useFormInputNameAttr } from '../../composables/use-form/private.use-form.js' import useFileFormDomProps from '../../composables/private.use-file/use-file-dom-props.js' import useKeyComposition from '../../composables/private.use-key-composition/use-key-composition.js' import { createComponent } from '../../utils/private.create/create.js' import { stop } from '../../utils/event/event.js' import { addFocusFn } from '../../utils/private.focus/focus-manager.js' import { injectProp } from '../../utils/private.inject-obj-prop/inject-obj-prop.js' export default createComponent({ name: 'QInput', inheritAttrs: false, props: { ...useFieldProps, ...useMaskProps, ...useFormProps, // override of useFieldProps > modelValue modelValue: __QUASAR_SSR_SERVER__ ? {} // SSR does not know about FileList : [String, Number, FileList], shadowText: String, type: { type: String, default: 'text' }, debounce: [String, Number], autogrow: Boolean, // makes a textarea inputClass: [Array, String, Object], inputStyle: [Array, String, Object] }, emits: [ ...useFieldEmits, 'paste', 'change', 'keydown', 'click', 'animationend' ], setup(props, { emit, attrs }) { const { proxy } = getCurrentInstance() const { $q } = proxy const temp = {} let emitCachedValue = NaN, typedNumber, stopValueWatcher, emitTimer = null, emitValueFn const inputRef = ref(null) const nameProp = useFormInputNameAttr(props) const { innerValue, hasMask, moveCursorForPaste, updateMaskValue, onMaskedKeydown, onMaskedClick } = useMask(props, emit, emitValue, inputRef) const formDomProps = useFileFormDomProps(props, /* type guard */ true) const hasValue = computed(() => fieldValueIsFilled(innerValue.value)) const onComposition = useKeyComposition(onInput) const state = useFieldState({ changeEvent: true }) const isTextarea = computed( () => props.type === 'textarea' || props.autogrow === true ) const isTypeText = computed( () => isTextarea.value === true || ['text', 'search', 'url', 'tel', 'password'].includes(props.type) ) const onEvents = computed(() => { const evt = { ...state.splitAttrs.listeners.value, onInput, onPaste, // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome // fires "change" instead of "input" on autocomplete. onChange, onBlur: onFinishEditing, onFocus: stop } evt.onCompositionstart = evt.onCompositionupdate = evt.onCompositionend = onComposition if (hasMask.value === true) { evt.onKeydown = onMaskedKeydown // reset selection anchor on pointer selection evt.onClick = onMaskedClick } if (props.autogrow === true) { evt.onAnimationend = onAnimationend } return evt }) const inputAttrs = computed(() => { const acc = { tabindex: 0, 'data-autofocus': props.autofocus === true || void 0, rows: props.type === 'textarea' ? 6 : void 0, 'aria-label': props.label, name: nameProp.value, ...state.splitAttrs.attributes.value, id: state.targetUid.value, maxlength: props.maxlength, disabled: props.disable === true, readonly: props.readonly === true } if (isTextarea.value === false) { acc.type = props.type } if (props.autogrow === true) { acc.rows = 1 } return acc }) // some browsers lose the native input value // so we need to reattach it dynamically // (like type="password" <-> type="text"; see #12078) watch( () => props.type, () => { if (inputRef.value) { inputRef.value.value = props.modelValue } } ) watch( () => props.modelValue, v => { if (hasMask.value === true) { if (stopValueWatcher === true) { stopValueWatcher = false if (String(v) === emitCachedValue) return } updateMaskValue(v) } else if (innerValue.value !== v) { innerValue.value = v if ( props.type === 'number' && temp.hasOwnProperty('value') === true ) { if (typedNumber === true) { typedNumber = false } else { delete temp.value } } } // textarea only if (props.autogrow === true) nextTick(adjustHeight) } ) watch( () => props.autogrow, val => { // textarea only if (val === true) { nextTick(adjustHeight) } // if it has a number of rows set respect it else if (inputRef.value !== null && attrs.rows > 0) { inputRef.value.style.height = 'auto' } } ) watch( () => props.dense, () => { if (props.autogrow === true) nextTick(adjustHeight) } ) function focus() { addFocusFn(() => { const el = document.activeElement if ( inputRef.value !== null && inputRef.value !== el && (el === null || el.id !== state.targetUid.value) ) { inputRef.value.focus({ preventScroll: true }) } }) } function select() { inputRef.value?.select() } function onPaste(e) { if (hasMask.value === true && props.reverseFillMask !== true) { const inp = e.target moveCursorForPaste(inp, inp.selectionStart, inp.selectionEnd) } emit('paste', e) } function onInput(e) { if (!e || !e.target) return if (props.type === 'file') { emit('update:modelValue', e.target.files) return } const val = e.target.value if (e.target.qComposing === true) { temp.value = val return } if (hasMask.value === true) { updateMaskValue(val, false, e.inputType) } else { emitValue(val) if (isTypeText.value === true && e.target === document.activeElement) { const { selectionStart, selectionEnd } = e.target if (selectionStart !== void 0 && selectionEnd !== void 0) { nextTick(() => { if ( e.target === document.activeElement && val.indexOf(e.target.value) === 0 ) { e.target.setSelectionRange(selectionStart, selectionEnd) } }) } } } // we need to trigger it immediately too, // to avoid "flickering" if (props.autogrow === true) adjustHeight() } function onAnimationend(e) { emit('animationend', e) adjustHeight() } function emitValue(val, stopWatcher) { emitValueFn = () => { emitTimer = null if (props.type !== 'number' && temp.hasOwnProperty('value') === true) { delete temp.value } if (props.modelValue !== val && emitCachedValue !== val) { emitCachedValue = val if (stopWatcher === true) stopValueWatcher = true emit('update:modelValue', val) nextTick(() => { if (emitCachedValue === val) emitCachedValue = NaN }) } emitValueFn = void 0 } if (props.type === 'number') { typedNumber = true temp.value = val } if (props.debounce !== void 0) { if (emitTimer !== null) clearTimeout(emitTimer) temp.value = val emitTimer = setTimeout(emitValueFn, props.debounce) } else { emitValueFn() } } // textarea only function adjustHeight() { requestAnimationFrame(() => { const inp = inputRef.value if (inp !== null) { const parentStyle = inp.parentNode.style // chrome does not keep scroll #15498 const { scrollTop } = inp // chrome calculates a smaller scrollHeight when in a .column container const { overflowY, maxHeight } = $q.platform.is.firefox === true ? {} : window.getComputedStyle(inp) // on firefox or if overflowY is specified as scroll #14263, #14344 // we don't touch overflow // firefox is not so bad in the end const changeOverflow = overflowY !== void 0 && overflowY !== 'scroll' // reset height of textarea to a small size to detect the real height // but keep the total control size the same if (changeOverflow === true) inp.style.overflowY = 'hidden' parentStyle.marginBottom = inp.scrollHeight - 1 + 'px' inp.style.height = '1px' inp.style.height = inp.scrollHeight + 'px' // we should allow scrollbars only // if there is maxHeight and content is taller than maxHeight if (changeOverflow === true) { inp.style.overflowY = parseInt(maxHeight, 10) < inp.scrollHeight ? 'auto' : 'hidden' } parentStyle.marginBottom = '' inp.scrollTop = scrollTop } }) } function onChange(e) { onComposition(e) if (emitTimer !== null) { clearTimeout(emitTimer) emitTimer = null } emitValueFn?.() emit('change', e.target.value) } function onFinishEditing(e) { if (e !== void 0) stop(e) if (emitTimer !== null) { clearTimeout(emitTimer) emitTimer = null } emitValueFn?.() typedNumber = false stopValueWatcher = false delete temp.value // we need to use setTimeout instead of this.$nextTick // to avoid a bug where focusout is not emitted for type date/time/week/... if (props.type !== 'file') { setTimeout(() => { if (inputRef.value !== null) { inputRef.value.value = innerValue.value !== void 0 ? innerValue.value : '' } }) } } function getCurValue() { return temp.hasOwnProperty('value') === true ? temp.value : innerValue.value !== void 0 ? innerValue.value : '' } onBeforeUnmount(() => { onFinishEditing() }) onMounted(() => { // textarea only if (props.autogrow === true) adjustHeight() }) Object.assign(state, { innerValue, fieldClass: computed( () => `q-${isTextarea.value === true ? 'textarea' : 'input'}` + (props.autogrow === true ? ' q-textarea--autogrow' : '') ), hasShadow: computed( () => props.type !== 'file' && typeof props.shadowText === 'string' && props.shadowText.length !== 0 ), inputRef, emitValue, hasValue, floatingLabel: computed( () => (hasValue.value === true && (props.type !== 'number' || isNaN(innerValue.value) === false)) || fieldValueIsFilled(props.displayValue) ), getControl: () => h(isTextarea.value === true ? 'textarea' : 'input', { ref: inputRef, class: ['q-field__native q-placeholder', props.inputClass], style: props.inputStyle, ...inputAttrs.value, ...onEvents.value, ...(props.type !== 'file' ? { value: getCurValue() } : formDomProps.value) }), getShadowControl: () => h( 'div', { class: 'q-field__native q-field__shadow absolute-bottom no-pointer-events' + (isTextarea.value === true ? '' : ' text-no-wrap') }, [ h('span', { class: 'invisible' }, getCurValue()), h('span', props.shadowText) ] ) }) const renderFn = useField(state) // expose public methods Object.assign(proxy, { focus, select, getNativeElement: () => inputRef.value // deprecated }) injectProp(proxy, 'nativeEl', () => inputRef.value) return renderFn } })