init
This commit is contained in:
125
uni_modules/nutui-uni/components/input/index.scss
Normal file
125
uni_modules/nutui-uni/components/input/index.scss
Normal file
@@ -0,0 +1,125 @@
|
||||
.nut-theme-dark {
|
||||
.nut-input {
|
||||
background: $dark-background;
|
||||
|
||||
&__input {
|
||||
color: $dark-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-input {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: $input-padding;
|
||||
font-size: $input-font-size;
|
||||
line-height: $input-line-height;
|
||||
background: $white;
|
||||
|
||||
&__left,
|
||||
&__right {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__left {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&__right {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__input {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
resize: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0 none;
|
||||
}
|
||||
|
||||
&__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__word-limit {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 10px;
|
||||
font-size: $input-limit-font-size;
|
||||
color: $input-limit-color;
|
||||
}
|
||||
|
||||
&__clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__clear-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 4px;
|
||||
line-height: 1;
|
||||
color: #c8c9cc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
color: $input-disabled-color !important;
|
||||
|
||||
input:disabled {
|
||||
color: $input-disabled-color;
|
||||
cursor: not-allowed;
|
||||
background: none;
|
||||
opacity: 1;
|
||||
-webkit-text-fill-color: $input-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--required {
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
color: $input-required-color;
|
||||
content: "*";
|
||||
}
|
||||
}
|
||||
|
||||
&--error,
|
||||
&--error::placeholder {
|
||||
color: $input-required-color;
|
||||
-webkit-text-fill-color: $input-required-color;
|
||||
}
|
||||
|
||||
&--border {
|
||||
border-bottom: 1px solid $input-border-bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-form-item {
|
||||
.nut-input {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: var(--nut-cell-line-height);
|
||||
}
|
||||
}
|
||||
2
uni_modules/nutui-uni/components/input/index.ts
Normal file
2
uni_modules/nutui-uni/components/input/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './input'
|
||||
export * from './type'
|
||||
177
uni_modules/nutui-uni/components/input/input.ts
Normal file
177
uni_modules/nutui-uni/components/input/input.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type {
|
||||
BaseEvent,
|
||||
InputOnBlurEvent,
|
||||
InputOnConfirmEvent,
|
||||
InputOnFocusEvent,
|
||||
InputOnInputEvent,
|
||||
} from '@uni-helper/uni-app-types'
|
||||
import type { ExtractPropTypes, PropType, StyleValue } from 'vue'
|
||||
import { BLUR_EVENT, CLEAR_EVENT, CLICK_EVENT, CONFIRM_EVENT, FOCUS_EVENT, INPUT_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
|
||||
import type { ClassType } from '../_utils'
|
||||
import {
|
||||
commonProps,
|
||||
isNumber,
|
||||
isString,
|
||||
makeNumberProp,
|
||||
makeNumericProp,
|
||||
makeStringProp,
|
||||
nullableBooleanProp,
|
||||
truthProp,
|
||||
} from '../_utils'
|
||||
import type { InputAlignType, InputConfirmType, InputFormatTrigger, InputMode, InputType } from './type'
|
||||
|
||||
export const inputProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 输入框类型,支持原生 `input` 标签的所有 `type` 属性,另外还支持 `number` `digit`
|
||||
*/
|
||||
type: makeStringProp<InputType>('text'),
|
||||
/**
|
||||
* @description 输入值,双向绑定
|
||||
*/
|
||||
modelValue: makeNumericProp(''),
|
||||
/**
|
||||
* @description 输入框自定义类名
|
||||
*/
|
||||
inputClass: {
|
||||
type: [String, Object, Array] as PropType<ClassType>,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* @description 输入框自定义样式
|
||||
*/
|
||||
inputStyle: {
|
||||
type: [String, Object, Array] as PropType<StyleValue>,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* @description 输入框为空时占位符
|
||||
*/
|
||||
placeholder: makeStringProp(''),
|
||||
/**
|
||||
* @description 指定 placeholder 的样式
|
||||
*/
|
||||
placeholderStyle: makeStringProp(''),
|
||||
/**
|
||||
* @description 指定 placeholder 的样式类
|
||||
*/
|
||||
placeholderClass: makeStringProp('input-placeholder'),
|
||||
/**
|
||||
* @description 输入框内容对齐方式,可选值 `left`、`center`、`right`
|
||||
*/
|
||||
inputAlign: makeStringProp<InputAlignType>('left'),
|
||||
/**
|
||||
* @description 是否显示必填字段的标签旁边的红色星号
|
||||
*/
|
||||
required: Boolean,
|
||||
/**
|
||||
* @description 是否禁用
|
||||
*/
|
||||
disabled: nullableBooleanProp,
|
||||
/**
|
||||
* @description 是否只读
|
||||
*/
|
||||
readonly: Boolean,
|
||||
/**
|
||||
* @description 是否标红
|
||||
*/
|
||||
error: Boolean,
|
||||
/**
|
||||
* @description 限制最长输入字符
|
||||
*/
|
||||
maxLength: makeNumericProp(140),
|
||||
/**
|
||||
* @description 展示清除 `Icon`
|
||||
*/
|
||||
clearable: Boolean,
|
||||
/**
|
||||
* @description 清除图标的 `font-size` 大小
|
||||
*/
|
||||
clearSize: makeNumericProp('14'),
|
||||
/**
|
||||
* @description 是否显示下边框
|
||||
*/
|
||||
border: truthProp,
|
||||
/**
|
||||
* @description 格式化函数触发的时机,可选值为 `onChange`、`onBlur`
|
||||
*/
|
||||
formatTrigger: makeStringProp<InputFormatTrigger>('onChange'),
|
||||
/**
|
||||
* @description 输入内容格式化函数
|
||||
*/
|
||||
formatter: {
|
||||
type: Function as PropType<(value: string) => string>,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* @description 是否显示限制最长输入字符,需要设置 `max-length` 属性
|
||||
*/
|
||||
showWordLimit: Boolean,
|
||||
/**
|
||||
* @description 是否自动获得焦点,`iOS` 系统不支持该属性
|
||||
*/
|
||||
autofocus: Boolean,
|
||||
/**
|
||||
* @description 键盘右下角按钮的文字,仅在`type='text'`时生效,可选值 `send`:发送、`search`:搜索、`next`:下一个、`go`:前往、`done`:完成
|
||||
*/
|
||||
confirmType: makeStringProp<InputConfirmType>('done'),
|
||||
/**
|
||||
* @description 键盘弹起时,是否自动上推页面
|
||||
*/
|
||||
adjustPosition: truthProp,
|
||||
/**
|
||||
* @description 是否强制使用系统键盘和 `Web-view` 创建的 `input` 元素。为 `true` 时,`confirm-type`、`confirm-hold` 可能失效
|
||||
*/
|
||||
alwaysSystem: Boolean,
|
||||
/**
|
||||
* @description 是否在失去焦点后,继续展示清除按钮,在设置 `clearable` 时生效
|
||||
*/
|
||||
showClearIcon: Boolean,
|
||||
/**
|
||||
* @description 输入框模式
|
||||
*/
|
||||
inputMode: makeStringProp<InputMode>('text'),
|
||||
/**
|
||||
* @description 指定光标与键盘的距离,取 input 距离底部的距离和 cursor-spacing 指定的距离的最小值作为光标与键盘的距离
|
||||
*/
|
||||
cursorSpacing: makeNumberProp(0),
|
||||
/**
|
||||
* @description 强制 input 处于同层状态,默认 focus 时 input 会切到非同层状态 (仅在 iOS 下生效)
|
||||
*/
|
||||
alwaysEmbed: Boolean,
|
||||
/**
|
||||
* @description 点击键盘右下角按钮时是否保持键盘不收起
|
||||
*/
|
||||
confirmHold: Boolean,
|
||||
/**
|
||||
* @description 指定focus时的光标位置
|
||||
*/
|
||||
cursor: Number,
|
||||
/**
|
||||
* @description 光标起始位置,自动聚集时有效,需与selection-end搭配使用
|
||||
*/
|
||||
selectionStart: makeNumberProp(-1),
|
||||
/**
|
||||
* @description 光标结束位置,自动聚集时有效,需与selection-start搭配使用
|
||||
*/
|
||||
selectionEnd: makeNumberProp(-1),
|
||||
/**
|
||||
* @description focus时,点击页面的时候不收起键盘
|
||||
*/
|
||||
holdKeyboard: Boolean,
|
||||
}
|
||||
|
||||
export type InputProps = ExtractPropTypes<typeof inputProps>
|
||||
|
||||
export const inputEmits = {
|
||||
[CLICK_EVENT]: (evt: BaseEvent) => evt instanceof Object,
|
||||
clickInput: (evt: BaseEvent) => evt instanceof Object,
|
||||
[BLUR_EVENT]: (evt: InputOnBlurEvent) => evt instanceof Object,
|
||||
[FOCUS_EVENT]: (evt: InputOnFocusEvent) => evt instanceof Object,
|
||||
[CLEAR_EVENT]: () => true,
|
||||
[CONFIRM_EVENT]: (evt: InputOnConfirmEvent) => evt instanceof Object,
|
||||
[UPDATE_MODEL_EVENT]: (val1: string | number, val2?: BaseEvent) => (isString(val1) || isNumber(val1)) && ((val2 instanceof Object) || val2 === undefined),
|
||||
[INPUT_EVENT]: (val: string | number, evt: InputOnInputEvent) => (isString(val) || isNumber(val)) && evt instanceof Object,
|
||||
}
|
||||
|
||||
export type InputEmits = typeof inputEmits
|
||||
273
uni_modules/nutui-uni/components/input/input.vue
Normal file
273
uni_modules/nutui-uni/components/input/input.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InputOnBlurEvent, InputOnConfirmEvent, InputOnFocusEvent, InputOnInputEvent } from '@uni-helper/uni-app-types'
|
||||
import { computed, defineComponent, nextTick, onMounted, ref, toRef, useSlots, watch } from 'vue'
|
||||
import { BLUR_EVENT, CLEAR_EVENT, CLICK_EVENT, CONFIRM_EVENT, FOCUS_EVENT, INPUT_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
|
||||
import { getMainClass, isH5 } from '../_utils'
|
||||
import { useFormDisabled } from '../form/form'
|
||||
import NutIcon from '../icon/icon.vue'
|
||||
import { inputEmits, inputProps } from './input'
|
||||
import type { InputFormatTrigger, InputTarget } from './type'
|
||||
import { formatNumber } from './util'
|
||||
|
||||
const props = defineProps(inputProps)
|
||||
|
||||
const emit = defineEmits(inputEmits)
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
function hasSlot(name: string) {
|
||||
return Boolean(slots[name])
|
||||
}
|
||||
|
||||
const formDisabled = useFormDisabled(toRef(props, 'disabled'))
|
||||
|
||||
function stringModelValue() {
|
||||
if (props.modelValue == null)
|
||||
return ''
|
||||
|
||||
return String(props.modelValue)
|
||||
}
|
||||
|
||||
const innerValue = computed<string>(() => {
|
||||
return stringModelValue()
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName, {
|
||||
[`${componentName}--disabled`]: formDisabled.value,
|
||||
[`${componentName}--required`]: props.required,
|
||||
[`${componentName}--error`]: props.error,
|
||||
[`${componentName}--border`]: props.border,
|
||||
})
|
||||
})
|
||||
|
||||
const inputStyles = computed(() => {
|
||||
return [props.inputStyle, {
|
||||
textAlign: props.inputAlign,
|
||||
}]
|
||||
})
|
||||
|
||||
const innerMaxLength = computed(() => {
|
||||
if (props.maxLength == null)
|
||||
return -1
|
||||
|
||||
return Number(props.maxLength)
|
||||
})
|
||||
|
||||
function updateValue(value: string, trigger: InputFormatTrigger = 'onChange') {
|
||||
if (innerMaxLength.value > 0 && value.length > innerMaxLength.value)
|
||||
value = value.slice(0, innerMaxLength.value)
|
||||
|
||||
if (props.type === 'number')
|
||||
value = formatNumber(value, false, false)
|
||||
|
||||
if (props.type === 'digit')
|
||||
value = formatNumber(value, true, true)
|
||||
|
||||
if (props.formatter && trigger === props.formatTrigger)
|
||||
value = props.formatter(value)
|
||||
|
||||
emit(UPDATE_MODEL_EVENT, value)
|
||||
}
|
||||
|
||||
function _onInput(evt: InputOnInputEvent) {
|
||||
updateValue(evt.detail.value)
|
||||
|
||||
nextTick(() => {
|
||||
emit(INPUT_EVENT, innerValue.value, evt)
|
||||
})
|
||||
}
|
||||
|
||||
function handleInput(evt: InputOnInputEvent) {
|
||||
if (isH5) {
|
||||
const target = evt.target as InputTarget
|
||||
|
||||
if (!target.composing)
|
||||
_onInput(evt)
|
||||
}
|
||||
else {
|
||||
_onInput(evt)
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(evt: any) {
|
||||
emit(CLICK_EVENT, evt)
|
||||
}
|
||||
|
||||
function handleClickInput(evt: any) {
|
||||
if (formDisabled.value)
|
||||
return
|
||||
|
||||
emit('clickInput', evt)
|
||||
}
|
||||
|
||||
const active = ref(false)
|
||||
|
||||
const clearing = ref(false)
|
||||
|
||||
function handleFocus(evt: InputOnFocusEvent) {
|
||||
if (formDisabled.value || props.readonly)
|
||||
return
|
||||
|
||||
emit(FOCUS_EVENT, evt)
|
||||
|
||||
active.value = true
|
||||
}
|
||||
|
||||
function handleBlur(evt: InputOnBlurEvent) {
|
||||
if (formDisabled.value || props.readonly)
|
||||
return
|
||||
|
||||
emit(BLUR_EVENT, evt)
|
||||
|
||||
setTimeout(() => {
|
||||
active.value = false
|
||||
}, 200)
|
||||
|
||||
if (clearing.value) {
|
||||
clearing.value = false
|
||||
return
|
||||
}
|
||||
|
||||
updateValue(evt.detail.value, 'onBlur')
|
||||
}
|
||||
|
||||
function handleConfirm(evt: InputOnConfirmEvent) {
|
||||
emit(CONFIRM_EVENT, evt)
|
||||
}
|
||||
|
||||
function handleClear(evt: any) {
|
||||
if (formDisabled.value)
|
||||
return
|
||||
|
||||
emit(UPDATE_MODEL_EVENT, '', evt)
|
||||
emit(CLEAR_EVENT)
|
||||
|
||||
clearing.value = true
|
||||
}
|
||||
|
||||
function startComposing(evt: any) {
|
||||
if (isH5) {
|
||||
const target = evt.target as InputTarget
|
||||
|
||||
target.composing = true
|
||||
}
|
||||
}
|
||||
|
||||
function endComposing(evt: any) {
|
||||
if (isH5) {
|
||||
const target = evt.target as InputTarget
|
||||
|
||||
if (target.composing) {
|
||||
target.composing = false
|
||||
target.dispatchEvent(new Event('input'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (value === innerValue.value)
|
||||
return
|
||||
|
||||
updateValue(stringModelValue())
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateValue(stringModelValue(), props.formatTrigger)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-input`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="classes" :style="props.customStyle" @click="handleClick">
|
||||
<view v-if="hasSlot('left')" class="nut-input__left">
|
||||
<slot name="left" />
|
||||
</view>
|
||||
|
||||
<view class="nut-input__value">
|
||||
<input
|
||||
class="nut-input__input"
|
||||
:class="props.inputClass"
|
||||
:style="inputStyles"
|
||||
:value="innerValue"
|
||||
:type="props.type as any"
|
||||
:placeholder="props.placeholder"
|
||||
:placeholder-style="props.placeholderStyle"
|
||||
:placeholder-class="props.placeholderClass"
|
||||
:disabled="formDisabled"
|
||||
:readonly="props.readonly"
|
||||
:focus="props.autofocus"
|
||||
:maxlength="innerMaxLength"
|
||||
:format-trigger="props.formatTrigger"
|
||||
:auto-blur="props.autofocus ? true : undefined"
|
||||
:confirm-type="props.confirmType"
|
||||
:adjust-position="props.adjustPosition"
|
||||
:always-system="props.alwaysSystem"
|
||||
:inputmode="props.inputMode"
|
||||
:cursor-spacing="props.cursorSpacing"
|
||||
:always-embed="props.alwaysEmbed"
|
||||
:confirm-hold="props.confirmHold"
|
||||
:cursor="props.cursor"
|
||||
:selection-start="props.selectionStart"
|
||||
:selection-end="props.selectionEnd"
|
||||
:hold-keyboard="props.holdKeyboard"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@click="handleClickInput"
|
||||
@change="endComposing"
|
||||
@compositionstart="startComposing"
|
||||
@compositionend="endComposing"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
|
||||
<view v-if="props.readonly" class="nut-input__mask" @click="handleClickInput" />
|
||||
|
||||
<view v-if="props.showWordLimit && innerMaxLength > 0" class="nut-input__word-limit">
|
||||
<text class="nut-input__word-num">
|
||||
{{ innerValue.length }}
|
||||
</text>/{{ innerMaxLength }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="props.clearable && !props.readonly"
|
||||
class="nut-input__clear"
|
||||
:class="{ 'nut-hidden': !((active || props.showClearIcon) && innerValue.length > 0) }"
|
||||
@click.stop="handleClear"
|
||||
>
|
||||
<slot v-if="hasSlot('clear')" name="clear" />
|
||||
<NutIcon
|
||||
v-else
|
||||
name="mask-close"
|
||||
custom-class="nut-input__clear-icon"
|
||||
:size="props.clearSize"
|
||||
:width="props.clearSize"
|
||||
:height="props.clearSize"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="hasSlot('right')" class="nut-input__right">
|
||||
<slot name="right" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
19
uni_modules/nutui-uni/components/input/type.ts
Normal file
19
uni_modules/nutui-uni/components/input/type.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const inputAlignType = ['left', 'center', 'right'] as const // text-align
|
||||
export type InputAlignType = (typeof inputAlignType)[number]
|
||||
|
||||
export const inputFormatTrigger = ['onChange', 'onBlur'] as const // onChange: 在输入时执行格式化 ; onBlur: 在失焦时执行格式化
|
||||
export type InputFormatTrigger = (typeof inputFormatTrigger)[number]
|
||||
|
||||
export const inputType
|
||||
= ['text', 'number', 'idcard', 'digit', 'tel', 'safe-password', 'nickname', 'button', 'checkbox', 'color', 'date', 'datetime-local', 'email', 'file', 'hidden', 'image', 'month', 'password', 'radio', 'range', 'reset', 'search', 'submit', 'time', 'url', 'week'] as const
|
||||
export type InputType = (typeof inputType)[number]
|
||||
|
||||
export const inputMode = ['none', 'text', 'decimal', 'numeric', 'tel', 'search', 'email', 'url'] as const
|
||||
export type InputMode = (typeof inputMode)[number]
|
||||
|
||||
export const inputConfirmType = ['send', 'search', 'next', 'go', 'done'] as const
|
||||
export type InputConfirmType = (typeof inputConfirmType)[number]
|
||||
|
||||
export interface InputTarget extends HTMLInputElement {
|
||||
composing?: boolean
|
||||
}
|
||||
29
uni_modules/nutui-uni/components/input/util.ts
Normal file
29
uni_modules/nutui-uni/components/input/util.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
function trimExtraChar(value: string, char: string, regExp: RegExp) {
|
||||
const index = value.indexOf(char)
|
||||
|
||||
if (index === -1)
|
||||
return value
|
||||
|
||||
if (char === '-' && index !== 0)
|
||||
return value.slice(0, index)
|
||||
|
||||
return value.slice(0, index + 1) + value.slice(index).replace(regExp, '')
|
||||
}
|
||||
|
||||
export function formatNumber(value: string, allowDot = true, allowMinus = true) {
|
||||
if (allowDot)
|
||||
value = trimExtraChar(value, '.', /\./g)
|
||||
|
||||
else
|
||||
value = value.split('.')[0]
|
||||
|
||||
if (allowMinus)
|
||||
value = trimExtraChar(value, '-', /-/g)
|
||||
|
||||
else
|
||||
value = value.replace(/-/, '')
|
||||
|
||||
const regExp = allowDot ? /[^-0-9.]/g : /[^-0-9]/g
|
||||
|
||||
return value.replace(regExp, '')
|
||||
}
|
||||
Reference in New Issue
Block a user