提交 8dc9937e 编写于 作者: Q qiang

refactor: input、textarea

上级 5f85117a
......@@ -7,7 +7,7 @@ import Editor from './editor/index'
import Form from './form/index'
import Icon from './icon/index'
import Image from './image/index'
import Input from './input/index.vue'
import Input from './input/index'
import Label from './label/index'
// import MovableArea from './movable-area/index.vue'
import MovableView from './movable-view/index.vue'
......@@ -25,7 +25,7 @@ import Slider from './slider/index.vue'
import SwiperItem from './swiper-item/index.vue'
import Switch from './switch/index.vue'
import Text from './text/index'
import Textarea from './textarea/index.vue'
import Textarea from './textarea/index'
import View from './view/index'
export {
Audio,
......
import { defineComponent, Ref, ref, computed } from 'vue'
import {
props as fieldProps,
emit as fieldEmit,
emit,
useField,
} from '../../helpers/useField'
const props = /*#__PURE__*/ Object.assign({}, fieldProps, {
placeholderClass: {
type: String,
default: 'input-placeholder',
},
})
export default /*#__PURE__*/ defineComponent({
name: 'Input',
props,
emit: ['confirm', ...fieldEmit],
setup(props, { emit }) {
const INPUT_TYPES = ['text', 'number', 'idcard', 'digit', 'password']
const type = computed(() => {
let type = ''
switch (props.type) {
case 'text':
if (props.confirmType === 'search') {
type = 'search'
}
break
case 'idcard':
// TODO 可能要根据不同平台进行区分处理
type = 'text'
break
case 'digit':
type = 'number'
break
default:
type = ~INPUT_TYPES.includes(props.type) ? props.type : 'text'
break
}
return props.password ? 'password' : type
})
const valid = ref(true)
let cachedValue = ''
const rootRef: Ref<HTMLElement | null> = ref(null)
const {
fieldRef,
state,
scopedAttrsState,
fixDisabledColor,
trigger,
} = useField(props, rootRef, emit, (event, state) => {
const input = event.target as HTMLInputElement
if (NUMBER_TYPES.includes(props.type)) {
// 在输入 - 负号 的情况下,event.target.value没有值,但是会触发校验 false,因此做此处理
valid.value = input.validity && input.validity.valid
cachedValue = state.value
// 处理部分输入法可以输入其它字符的情况
// 上一处理导致无法输入 - ,因此去除
// if (input.validity && !input.validity.valid) {
// input.value = cachedValue
// state.value = input.value
// // 输入非法字符不触发 input 事件
// return false
// } else {
// cachedValue = state.value
// }
}
// type="number" 不支持 maxlength 属性,因此需要主动限制长度。
if (type.value === 'number') {
const maxlength = state.maxlength
if (maxlength > 0 && input.value.length > maxlength) {
input.value = input.value.slice(0, maxlength)
state.value = input.value
// 字符长度超出范围不触发 input 事件
return false
}
}
})
const NUMBER_TYPES = ['number', 'digit']
const step = computed(() =>
NUMBER_TYPES.includes(props.type) ? '0.000000000000000001' : ''
)
function onKeyUpEnter(event: Event) {
if ((event as KeyboardEvent).key !== 'Enter') {
return
}
event.stopPropagation()
trigger('confirm', event, {
value: (event.target as HTMLInputElement).value,
})
}
return () => {
let inputNode =
props.disabled && fixDisabledColor ? (
<input
ref={fieldRef}
value={state.value}
tabindex="-1"
readonly={!!props.disabled}
type={type.value}
maxlength={state.maxlength}
step={step.value}
class="uni-input-input"
// fix: 禁止 readonly 状态获取焦点
onFocus={(event: Event) =>
(event.target as HTMLInputElement).blur()
}
/>
) : (
<input
ref={fieldRef}
value={state.value}
disabled={!!props.disabled}
type={type.value}
maxlength={state.maxlength}
step={step.value}
enterkeyhint={props.confirmType}
class="uni-input-input"
autocomplete="off"
onKeyup={onKeyUpEnter}
/>
)
return (
<uni-input ref={rootRef}>
<div class="uni-input-wrapper">
<div
v-show={!(state.value.length || !valid.value)}
{...scopedAttrsState.attrs}
style={props.placeholderStyle}
class={['uni-input-placeholder', props.placeholderClass]}
>
{props.placeholder}
</div>
{props.confirmType === 'search' ? (
<form action="" onSubmit={() => false} class="uni-input-form">
{inputNode}
</form>
) : (
inputNode
)}
</div>
</uni-input>
)
}
},
})
<template>
<uni-input @change.stop v-bind="$attrs">
<div ref="wrapper" class="uni-input-wrapper">
<div
v-show="!(composing || valueSync.length)"
ref="placeholder"
:style="placeholderStyle"
:class="placeholderClass"
class="uni-input-placeholder"
v-text="placeholder"
/>
<input
ref="input"
v-model="valueSync"
:disabled="disabled"
:type="inputType"
:maxlength="maxlength"
:step="step"
:autofocus="focus"
class="uni-input-input"
autocomplete="off"
@focus="_onFocus"
@blur="_onBlur"
@input.stop="_onInput"
@compositionstart="_onComposition"
@compositionend="_onComposition"
@keyup.stop="_onKeyup"
/>
</div>
</uni-input>
</template>
<script>
import { getCurrentInstance } from 'vue'
import { useFormField } from '../../helpers/useFormField'
import {
baseInput
} from '../../mixins'
const INPUT_TYPES = ['text', 'number', 'idcard', 'digit', 'password']
const NUMBER_TYPES = ['number', 'digit']
export default {
name: 'Input',
mixins: [baseInput],
props: {
name: {
type: String,
default: ''
},
type: {
type: String,
default: 'text'
},
password: {
type: [Boolean, String],
default: false
},
placeholder: {
type: String,
default: ''
},
placeholderStyle: {
type: String,
default: ''
},
placeholderClass: {
type: String,
default: 'input-placeholder'
},
disabled: {
type: [Boolean, String],
default: false
},
maxlength: {
type: [Number, String],
default: 140
},
focus: {
type: [Boolean, String],
default: false
},
confirmType: {
type: String,
default: 'done'
}
},
data() {
return {
composing: false,
wrapperHeight: 0,
cachedValue: ''
}
},
computed: {
inputType: function() {
let type = ''
switch (this.type) {
case 'text':
this.confirmType === 'search' && (type = 'search')
break
case 'idcard':
// TODO 可能要根据不同平台进行区分处理
type = 'text'
break
case 'digit':
type = 'number'
break
default:
type = ~INPUT_TYPES.indexOf(this.type) ? this.type : 'text'
break
}
return this.password ? 'password' : type
},
step() {
// 处理部分设备中无法输入小数点的问题
return ~NUMBER_TYPES.indexOf(this.type) ? '0.000000000000000001' : ''
}
},
watch: {
focus(val) {
this.$refs.input && this.$refs.input[val ? 'focus' : 'blur']()
},
maxlength(value) {
const realValue = this.valueSync.slice(0, parseInt(value, 10))
realValue !== this.valueSync && (this.valueSync = realValue)
}
},
setup() {
useFormField('name', 'valueSync')
},
mounted() {
if (this.confirmType === 'search') {
const formElem = document.createElement('form')
formElem.action = ''
formElem.onsubmit = function() {
return false
}
formElem.className = 'uni-input-form'
formElem.appendChild(this.$refs.input)
this.$refs.wrapper.appendChild(formElem)
}
const instance = getCurrentInstance()
if (instance && instance.vnode.scopeId) {
this.$refs.placeholder.setAttribute(instance.vnode.scopeId, '')
}
// let $vm = this
// while ($vm) {
// const scopeId = $vm.$options._scopeId
// if (scopeId) {
// this.$refs.placeholder.setAttribute(scopeId, '')
// }
// $vm = $vm.$parent
// }
this.initKeyboard(this.$refs.input)
},
methods: {
_onKeyup($event) {
if ($event.keyCode === 13) {
this.$trigger('confirm', $event, {
value: $event.target.value
})
}
},
_onInput($event) {
if (this.composing) {
return
}
// 处理部分输入法可以输入其它字符的情况
if (~NUMBER_TYPES.indexOf(this.type)) {
if (this.$refs.input.validity && !this.$refs.input.validity.valid) {
$event.target.value = this.cachedValue
this.valueSync = $event.target.value
// 输入非法字符不触发 input 事件
return
} else {
this.cachedValue = this.valueSync
}
}
// type="number" 不支持 maxlength 属性,因此需要主动限制长度。
if (this.inputType === 'number') {
const maxlength = parseInt(this.maxlength, 10)
if (maxlength > 0 && $event.target.value.length > maxlength) {
$event.target.value = $event.target.value.slice(0, maxlength)
this.valueSync = $event.target.value
// 字符长度超出范围不触发 input 事件
return
}
}
this.$triggerInput($event, {
value: this.valueSync
})
},
_onFocus($event) {
this.$trigger('focus', $event, {
value: $event.target.value
})
},
_onBlur($event) {
this.$trigger('blur', $event, {
value: $event.target.value
})
},
_onComposition($event) {
if ($event.type === 'compositionstart') {
this.composing = true
} else {
this.composing = false
}
},
_resetFormData() {
this.valueSync = ''
},
_getFormData() {
return this.name ? {
value: this.valueSync,
key: this.name
} : {}
}
}
}
</script>
import { defineComponent, Ref, ref, computed, watch } from 'vue'
import {
props as fieldProps,
emit as fieldEmit,
useField,
} from '../../helpers/useField'
import ResizeSensor from '../resize-sensor/index'
const props = /*#__PURE__*/ Object.assign({}, fieldProps, {
placeholderClass: {
type: String,
default: 'input-placeholder',
},
autoHeight: {
type: [Boolean, String],
default: false,
},
confirmType: {
type: String,
default: '',
},
})
export default /*#__PURE__*/ defineComponent({
name: 'Textarea',
props,
emit: ['confirm', 'linechange', ...fieldEmit],
setup(props, { emit }) {
const rootRef: Ref<HTMLElement | null> = ref(null)
const {
fieldRef,
state,
scopedAttrsState,
fixDisabledColor,
trigger,
} = useField(props, rootRef, emit)
const valueCompute = computed(() => state.value.split('\n'))
const isDone = computed(() =>
['done', 'go', 'next', 'search', 'send'].includes(props.confirmType)
)
const heightRef = ref(0)
const lineRef: Ref<HTMLElement | null> = ref(null)
watch(
() => heightRef.value,
(height) => {
const el = rootRef.value as HTMLElement
const lineEl = lineRef.value as HTMLElement
let lineHeight = parseFloat(getComputedStyle(el).lineHeight)
if (isNaN(lineHeight)) {
lineHeight = lineEl.offsetHeight
}
var lineCount = Math.round(height / lineHeight)
trigger('linechange', {} as Event, {
height,
heightRpx: (750 / window.innerWidth) * height,
lineCount,
})
if (props.autoHeight) {
el.style.height = height + 'px'
}
}
)
function onResize({ height }: { height: number }) {
heightRef.value = height
}
function confirm(event: Event) {
trigger('confirm', event, {
value: state.value,
})
}
function onKeyDownEnter(event: Event) {
if ((event as KeyboardEvent).key !== 'Enter') {
return
}
if (isDone.value) {
event.preventDefault()
}
}
function onKeyUpEnter(event: Event) {
if ((event as KeyboardEvent).key !== 'Enter') {
return
}
if (isDone.value) {
confirm(event)
const textarea = event.target as HTMLTextAreaElement
textarea.blur()
}
}
// iOS 13 以下版本需要修正边距
const DARK_TEST_STRING = '(prefers-color-scheme: dark)'
const fixMargin =
String(navigator.platform).indexOf('iP') === 0 &&
String(navigator.vendor).indexOf('Apple') === 0 &&
window.matchMedia(DARK_TEST_STRING).media !== DARK_TEST_STRING
return () => {
let textareaNode =
props.disabled && fixDisabledColor ? (
<textarea
ref={fieldRef}
value={state.value}
tabindex="-1"
readonly={!!props.disabled}
maxlength={state.maxlength}
class={{
'uni-textarea-textarea': true,
'uni-textarea-textarea-fix-margin': fixMargin,
}}
style={{ overflowY: props.autoHeight ? 'hidden' : 'auto' }}
// fix: 禁止 readonly 状态获取焦点
onFocus={(event: Event) =>
(event.target as HTMLInputElement).blur()
}
/>
) : (
<textarea
ref={fieldRef}
value={state.value}
disabled={!!props.disabled}
maxlength={state.maxlength}
enterkeyhint={props.confirmType}
class={{
'uni-textarea-textarea': true,
'uni-textarea-textarea-fix-margin': fixMargin,
}}
style={{ overflowY: props.autoHeight ? 'hidden' : 'auto' }}
onKeydown={onKeyDownEnter}
onKeyup={onKeyUpEnter}
/>
)
return (
<uni-textarea ref={rootRef}>
<div class="uni-textarea-wrapper">
<div
v-show={!state.value.length}
{...scopedAttrsState.attrs}
style={props.placeholderStyle}
class={['uni-textarea-placeholder', props.placeholderClass]}
>
{props.placeholder}
</div>
<div ref={lineRef} class="uni-textarea-line">
{' '}
</div>
<div class="uni-textarea-compute">
{valueCompute.value.map((item) => (
<div>{item.trim() ? item : '.'}</div>
))}
<ResizeSensor initial onResize={onResize} />
</div>
{props.confirmType === 'search' ? (
<form action="" onSubmit={() => false} class="uni-input-form">
{textareaNode}
</form>
) : (
textareaNode
)}
</div>
</uni-textarea>
)
}
},
})
<template>
<uni-textarea
@change.stop
v-bind="$attrs"
>
<div class="uni-textarea-wrapper">
<div
v-show="!(composition||valueSync.length)"
ref="placeholder"
:style="placeholderStyle"
:class="placeholderClass"
class="uni-textarea-placeholder"
v-text="placeholder"
/>
<div
ref="line"
class="uni-textarea-line"
v-text="' '"
/>
<div class="uni-textarea-compute">
<div
v-for="(item,index) in valueCompute"
:key="index"
v-text="item.trim() ? item : '.'"
/>
<v-uni-resize-sensor
ref="sensor"
@resize="_resize"
/>
</div>
<textarea
ref="textarea"
v-model="valueSync"
:disabled="disabled"
:maxlength="maxlengthNumber"
:autofocus="autoFocus || focus"
:class="{'uni-textarea-textarea-fix-margin': fixMargin}"
:style="{'overflow-y': autoHeight? 'hidden':'auto'}"
class="uni-textarea-textarea"
@compositionstart="_compositionstart"
@compositionend="_compositionend"
@input.stop="_input"
@focus="_focus"
@blur="_blur"
@touchstart.passive="_touchstart"
/>
</div>
</uni-textarea>
</template>
<script>
import {
baseInput
} from '../../mixins'
const DARK_TEST_STRING = '(prefers-color-scheme: dark)'
export default {
name: 'Textarea',
mixins: [baseInput],
props: {
name: {
type: String,
default: ''
},
maxlength: {
type: [Number, String],
default: 140
},
placeholder: {
type: String,
default: ''
},
disabled: {
type: [Boolean, String],
default: false
},
focus: {
type: [Boolean, String],
default: false
},
autoFocus: {
type: [Boolean, String],
default: false
},
placeholderClass: {
type: String,
default: 'textarea-placeholder'
},
placeholderStyle: {
type: String,
default: ''
},
autoHeight: {
type: [Boolean, String],
default: false
},
cursor: {
type: [Number, String],
default: -1
},
selectionStart: {
type: [Number, String],
default: -1
},
selectionEnd: {
type: [Number, String],
default: -1
}
},
data () {
return {
valueComposition: '',
composition: false,
focusSync: this.focus,
height: 0,
focusChangeSource: '',
// iOS 13 以下版本需要修正边距
fixMargin: String(navigator.platform).indexOf('iP') === 0 && String(navigator.vendor).indexOf('Apple') === 0 && window.matchMedia(DARK_TEST_STRING).media !== DARK_TEST_STRING
}
},
computed: {
maxlengthNumber () {
var maxlength = Number(this.maxlength)
return isNaN(maxlength) ? 140 : maxlength
},
cursorNumber () {
var cursor = Number(this.cursor)
return isNaN(cursor) ? -1 : cursor
},
selectionStartNumber () {
var selectionStart = Number(this.selectionStart)
return isNaN(selectionStart) ? -1 : selectionStart
},
selectionEndNumber () {
var selectionEnd = Number(this.selectionEnd)
return isNaN(selectionEnd) ? -1 : selectionEnd
},
valueCompute () {
return (this.composition ? this.valueComposition : this.valueSync).split('\n')
}
},
watch: {
focus (val) {
if (val) {
this.focusChangeSource = 'focus'
if (this.$refs.textarea) {
this.$refs.textarea.focus()
}
} else {
if (this.$refs.textarea) {
this.$refs.textarea.blur()
}
}
},
focusSync (val) {
this.$emit('update:focus', val)
this._checkSelection()
this._checkCursor()
},
cursorNumber () {
this._checkCursor()
},
selectionStartNumber () {
this._checkSelection()
},
selectionEndNumber () {
this._checkSelection()
},
height (height) {
let lineHeight = parseFloat(getComputedStyle(this.$el).lineHeight)
if (isNaN(lineHeight)) {
lineHeight = this.$refs.line.offsetHeight
}
var lineCount = Math.round(height / lineHeight)
this.$trigger('linechange', {}, {
height,
heightRpx: 750 / window.innerWidth * height,
lineCount
})
if (this.autoHeight) {
this.$el.style.height = this.height + 'px'
}
}
},
created () {
this.$dispatch('Form', 'uni-form-group-update', {
type: 'add',
vm: this
})
},
mounted () {
// this._resize({
// height: this.$refs.sensor.$el.offsetHeight
// })
let $vm = this
while ($vm) {
const scopeId = $vm.$options._scopeId
if (scopeId) {
this.$refs.placeholder.setAttribute(scopeId, '')
}
$vm = $vm.$parent
}
this.initKeyboard(this.$refs.textarea)
},
beforeDestroy () {
this.$dispatch('Form', 'uni-form-group-update', {
type: 'remove',
vm: this
})
},
methods: {
_focus: function ($event) {
this.focusSync = true
this.$trigger('focus', $event, {
value: this.valueSync
})
},
_checkSelection () {
if (this.focusSync && (!this.focusChangeSource) && this.selectionStartNumber > -1 && this.selectionEndNumber > -1) {
this.$refs.textarea.selectionStart = this.selectionStartNumber
this.$refs.textarea.selectionEnd = this.selectionEndNumber
}
},
_checkCursor () {
if (this.focusSync && (this.focusChangeSource === 'focus' || ((!this.focusChangeSource) && this.selectionStartNumber < 0 && this.selectionEndNumber < 0)) && this.cursorNumber > -1) {
this.$refs.textarea.selectionEnd = this.$refs.textarea.selectionStart = this.cursorNumber
}
},
_blur: function ($event) {
this.focusSync = false
this.$trigger('blur', $event, {
value: this.valueSync,
cursor: this.$refs.textarea.selectionEnd
})
},
_compositionstart ($event) {
this.composition = true
},
_compositionend ($event) {
this.composition = false
},
// 暂无完成按钮,此功能未实现
_confirm ($event) {
this.$trigger('confirm', $event, {
value: this.valueSync
})
},
_linechange ($event) {
this.$trigger('linechange', $event, {
value: this.valueSync
})
},
_touchstart () {
this.focusChangeSource = 'touch'
},
_resize ({ height }) {
this.height = height
},
_input ($event) {
if (this.composition) {
this.valueComposition = $event.target.value
return
}
this.$triggerInput($event, {
value: this.valueSync,
cursor: this.$refs.textarea.selectionEnd
})
},
_getFormData () {
return {
value: this.valueSync,
key: this.name
}
},
_resetFormData () {
this.valueSync = ''
}
}
}
</script>
\ No newline at end of file
export function throttle(fn, wait) {
export function throttle(fn: Function, wait: number) {
let last = 0
let timeout
const newFn = function(...arg) {
let timeout: number
let waitCallback: Function | null
const newFn = function (this: any, ...arg: any) {
const now = Date.now()
clearTimeout(timeout)
const waitCallback = () => {
waitCallback = () => {
waitCallback = null
last = now
fn.apply(this, arg)
}
......@@ -14,8 +16,13 @@ export function throttle(fn, wait) {
}
waitCallback()
}
newFn.cancel = function() {
newFn.cancel = function () {
clearTimeout(timeout)
waitCallback = null
}
newFn.flush = function () {
clearTimeout(timeout)
waitCallback && waitCallback()
}
return newFn
}
import {
Ref,
ref,
SetupContext,
watch,
onMounted,
onBeforeMount,
computed,
reactive,
} from 'vue'
import { debounce } from '@dcloudio/uni-shared'
import { throttle } from './throttle'
import { useCustomEvent, CustomEventTrigger } from './useEvent'
import { useUserAction } from './useUserAction'
import {
props as keyboardProps,
emit as keyboardEmit,
useKeyboard,
} from './useKeyboard'
import { useScopedAttrs } from './useScopedAttrs'
import { useFormField } from './useFormField'
// App 延迟获取焦点
const FOCUS_DELAY = 200
let startTime: number
function getValueString(value: any) {
return value === null ? '' : String(value)
}
interface InputEventDetail {
value: string
}
type HTMLFieldElement = HTMLInputElement | HTMLTextAreaElement
export const props = /*#__PURE__*/ Object.assign(
{},
{
name: {
type: String,
default: '',
},
value: {
type: [String, Number],
default: '',
},
disabled: {
type: [Boolean, String],
default: false,
},
/**
* 已废弃属性,用于历史兼容
*/
autoFocus: {
type: [Boolean, String],
default: false,
},
focus: {
type: [Boolean, String],
default: false,
},
cursor: {
type: [Number, String],
default: -1,
},
selectionStart: {
type: [Number, String],
default: -1,
},
selectionEnd: {
type: [Number, String],
default: -1,
},
type: {
type: String,
default: 'text',
},
password: {
type: [Boolean, String],
default: false,
},
placeholder: {
type: String,
default: '',
},
placeholderStyle: {
type: String,
default: '',
},
placeholderClass: {
type: String,
default: '',
},
maxlength: {
type: [Number, String],
default: 140,
},
confirmType: {
type: String,
default: 'done',
},
},
keyboardProps
)
export const emit = ['input', 'focus', 'blur', ...keyboardEmit]
type Props = Record<keyof typeof props, any>
interface State {
value: string
maxlength: number
focus: boolean
composing: boolean
selectionStart: number
selectionEnd: number
cursor: number
}
function useBase(
props: Props,
rootRef: Ref<HTMLElement | null>,
emit: SetupContext['emit']
) {
const fieldRef: Ref<HTMLFieldElement | null> = ref(null)
const trigger = useCustomEvent(rootRef, emit)
const selectionStart = computed(() => {
const selectionStart = Number(props.selectionStart)
return isNaN(selectionStart) ? -1 : selectionStart
})
const selectionEnd = computed(() => {
const selectionEnd = Number(props.selectionEnd)
return isNaN(selectionEnd) ? -1 : selectionEnd
})
const cursor = computed(() => {
const cursor = Number(props.cursor)
return isNaN(cursor) ? -1 : cursor
})
const maxlength = computed(() => {
var maxlength = Number(props.maxlength)
return isNaN(maxlength) ? 140 : maxlength
})
const value = getValueString(props.value)
const state: State = reactive({
value,
valueOrigin: value,
maxlength,
focus: props.focus,
composing: false,
selectionStart,
selectionEnd,
cursor,
})
watch(
() => state.focus,
(val) => emit('update:focus', val)
)
watch(
() => state.maxlength,
(val) => (state.value = state.value.slice(0, val))
)
return {
fieldRef,
state,
trigger,
}
}
function useValueSync(
props: Props,
state: { value: string },
emit: SetupContext['emit'],
trigger: CustomEventTrigger
) {
const valueChangeFn = debounce((val: any) => {
state.value = getValueString(val)
}, 100)
watch(() => props.value, valueChangeFn)
const triggerInputFn = throttle((event: Event, detail: InputEventDetail) => {
emit('update:value', detail.value)
trigger('input', event, detail)
}, 100)
const triggerInput = (
event: Event,
detail: InputEventDetail,
force: boolean
) => {
valueChangeFn.cancel()
triggerInputFn(event, detail)
if (force) {
triggerInputFn.flush()
}
}
onBeforeMount(() => {
valueChangeFn.cancel()
triggerInputFn.cancel()
})
return {
trigger,
triggerInput,
}
}
function useAutoFocus(props: Props, fieldRef: Ref<HTMLFieldElement | null>) {
const { state: userActionState } = useUserAction()
const needFocus = computed(() => props.autoFocus || props.focus)
function focus() {
if (!needFocus.value) {
return
}
const field = fieldRef.value
if (!field || (__PLATFORM__ === 'app' && !('plus' in window))) {
setTimeout(focus, 100)
return
}
if (__PLATFORM__ === 'h5') {
field.focus()
} else {
const timeout = FOCUS_DELAY - (Date.now() - startTime)
if (timeout > 0) {
setTimeout(focus, timeout)
return
}
field.focus()
// 无用户交互的 webview 需主动显示键盘(安卓)
if (!userActionState.userAction) {
plus.key.showSoftKeybord()
}
}
}
function blur() {
const field = fieldRef.value
if (field) {
field.blur()
}
}
watch(
() => props.focus,
(value) => {
if (value) {
focus()
} else {
blur()
}
}
)
onMounted(() => {
startTime = startTime || Date.now()
if (needFocus.value) {
focus()
}
})
}
function useEvent(
fieldRef: Ref<HTMLFieldElement | null>,
state: State,
trigger: CustomEventTrigger,
triggerInput: Function,
beforeInput?: (event: Event, state: State) => any
) {
function checkSelection() {
const field = fieldRef.value
if (
field &&
state.focus &&
state.selectionStart > -1 &&
state.selectionEnd > -1
) {
field.selectionStart = state.selectionStart
field.selectionEnd = state.selectionEnd
}
}
function checkCursor() {
const field = fieldRef.value
if (
field &&
state.focus &&
state.selectionStart < 0 &&
state.selectionEnd < 0 &&
state.cursor > -1
) {
field.selectionEnd = field.selectionStart = state.cursor
}
}
function initField() {
const field = fieldRef.value as HTMLFieldElement
const onFocus = function (event: Event) {
state.focus = true
trigger('focus', event, {
value: state.value,
})
// 从 watch:focusSync 中移出到这里。在watcher中如果focus初始值为ture,则不会执行以下逻辑
checkSelection()
checkCursor()
}
const onInput = function (event: Event, force?: boolean) {
event.stopPropagation()
if (
typeof beforeInput === 'function' &&
beforeInput(event, state) === false
) {
return
}
state.value = field.value
if (!state.composing) {
triggerInput(
event,
{
value: field.value,
cursor: field.selectionEnd,
},
force
)
}
}
const onBlur = function (event: Event) {
// iOS 输入法 compositionend 事件可能晚于 blur
if (state.composing) {
state.composing = false
onInput(event, true)
}
state.focus = false
trigger('blur', event, {
value: state.value,
cursor: (event.target as HTMLFieldElement).selectionEnd,
})
}
// 避免触发父组件 change 事件
field.addEventListener('change', (event: Event) => event.stopPropagation())
field.addEventListener('focus', onFocus)
field.addEventListener('blur', onBlur)
field.addEventListener('input', onInput)
field.addEventListener('compositionstart', (event) => {
event.stopPropagation()
state.composing = true
})
field.addEventListener('compositionend', (event) => {
event.stopPropagation()
if (state.composing) {
state.composing = false
// 部分输入法 compositionend 事件可能晚于 input
onInput(event)
}
})
}
watch([() => state.selectionStart, () => state.selectionEnd], checkSelection)
watch(() => state.cursor, checkCursor)
watch(() => fieldRef.value, initField)
}
export function useField(
props: Props,
rootRef: Ref<HTMLElement | null>,
emit: SetupContext['emit'],
beforeInput?: (event: Event, state: State) => any
) {
const { fieldRef, state, trigger } = useBase(props, rootRef, emit)
const { triggerInput } = useValueSync(props, state, emit, trigger)
useAutoFocus(props, fieldRef)
useKeyboard(props, fieldRef, trigger)
const { state: scopedAttrsState } = useScopedAttrs()
useFormField('name', state)
useEvent(fieldRef, state, trigger, triggerInput, beforeInput)
// Safari 14 以上修正禁用状态颜色
const fixDisabledColor =
String(navigator.vendor).indexOf('Apple') === 0 &&
CSS.supports('image-orientation:from-image')
return {
fieldRef,
state,
scopedAttrsState,
fixDisabledColor,
trigger,
}
}
import { getCurrentInstance, inject, onBeforeUnmount } from 'vue'
import { UniFormCtx, uniFormKey } from '../components/form'
export function useFormField(nameKey: string, valueKey: string) {
interface ValueState {
value: string
}
export function useFormField(nameKey: string, valueKey: string): void
export function useFormField(nameKey: string, valueState: ValueState): void
export function useFormField(
nameKey: string,
value: string | ValueState
): void {
const uniForm = inject<UniFormCtx>(
uniFormKey,
(false as unknown) as UniFormCtx // remove warning
......@@ -13,10 +22,17 @@ export function useFormField(nameKey: string, valueKey: string) {
const ctx = {
submit(): [string, any] {
const proxy = instance.proxy
return [(proxy as any)[nameKey], (proxy as any)[valueKey]]
return [
(proxy as any)[nameKey],
typeof value === 'string' ? (proxy as any)[value] : value.value,
]
},
reset() {
;(instance.proxy as any)[valueKey] = ''
if (typeof value === 'string') {
;(instance.proxy as any)[value] = ''
} else {
value.value = ''
}
},
}
uniForm.addField(ctx)
......
import { Ref, onMounted } from 'vue'
import { Ref, watch } from 'vue'
import { CustomEventTrigger } from './useEvent'
import { plusReady } from '@dcloudio/uni-shared'
......@@ -101,14 +101,34 @@ interface KeyboardState {
softinputNavBar?: any
}
export function useKeyboard(
props: {
disabled: any
cursorSpacing: number | string
autoBlur: any
adjustPosition: any
showConfirmBar: any
export const props = {
cursorSpacing: {
type: [Number, String],
default: 0,
},
showConfirmBar: {
type: [Boolean, String],
default: 'auto',
},
adjustPosition: {
type: [Boolean, String],
default: true,
},
autoBlur: {
type: [Boolean, String],
default: false,
},
}
export const emit = ['keyboardheightchange']
interface Props extends Record<keyof typeof props, any> {
disabled?: any
readOnly?: any
}
export function useKeyboard(
props: Props,
elRef: Ref<HTMLElement | null>,
trigger: CustomEventTrigger
) {
......@@ -157,14 +177,19 @@ export function useKeyboard(
if (__PLATFORM__ === 'app') {
// 安卓单独隐藏键盘后点击输入框不会触发 focus 事件
el.addEventListener('click', () => {
if (!props.disabled && focus && keyboardHeight === 0) {
if (
!props.disabled &&
!props.readOnly &&
focus &&
keyboardHeight === 0
) {
setSoftinputTemporary(props, el)
}
})
if (!isAndroid && parseInt(osVersion) < 12) {
// iOS12 以下系统 focus 事件设置较迟,改在 touchstart 设置
el.addEventListener('touchstart', () => {
if (!props.disabled && !focus) {
if (!props.disabled && !props.readOnly && !focus) {
setSoftinputTemporary(props, el)
}
})
......@@ -205,8 +230,8 @@ export function useKeyboard(
onKeyboardHide()
})
}
onMounted(() => {
const el = elRef.value as HTMLElement
initKeyboard(el)
})
watch(
() => elRef.value,
(el) => initKeyboard(el as HTMLElement)
)
}
import {
onMounted,
getCurrentInstance,
ComponentInternalInstance,
reactive,
} from 'vue'
interface State {
attrs: Record<string, string>
}
export function useScopedAttrs() {
const state: State = reactive({
attrs: {},
})
onMounted(() => {
let vm = (getCurrentInstance() as ComponentInternalInstance).proxy
while (vm) {
const $options = vm.$options
const scopeId = $options.__scopeId
if (scopeId) {
const attrs: Record<string, string> = {}
attrs[scopeId] = ''
state.attrs = attrs
}
vm = vm.$parent
}
})
return {
state,
}
}
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册