This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
.nut-theme-dark {
.nut-textarea {
background: $dark-background;
&__textarea {
color: $dark-color;
}
}
}
.nut-textarea {
position: relative;
box-sizing: border-box;
display: flex;
width: 100%;
padding: 10px 25px;
font-size: $textarea-font;
background: $white;
&--disabled {
.nut-textarea__textarea,
// TODO PR原版类名错误
.nut-textarea__limit {
color: $textarea-disabled-color !important;
cursor: not-allowed;
}
}
&__limit {
position: absolute;
right: 15px;
bottom: 12px;
font-size: $textarea-font;
color: $textarea-limit-color;
}
&__textarea {
box-sizing: border-box;
display: block;
width: 100%;
min-width: 0;
padding: 0;
margin: 0;
font-size: $textarea-font;
line-height: 20px;
color: $textarea-text-color;
text-align: left;
resize: none;
background-color: transparent;
border: none;
outline: none;
.taro-textarea {
font-size: 14px;
}
}
&__ali {
line-height: 17px;
}
}

View File

@@ -0,0 +1,2 @@
export * from './textarea'
export * from './type'

View File

@@ -0,0 +1,133 @@
import type { TextareaOnBlurEvent, TextareaOnConfirmEvent, TextareaOnFocusEvent, TextareaOnInputEvent } from '@uni-helper/uni-app-types'
import type { ExtractPropTypes, PropType, StyleValue } from 'vue'
import { BLUR_EVENT, CHANGE_EVENT, CONFIRM_EVENT, FOCUS_EVENT, INPUT_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
import type { ClassType } from '../_utils'
import { commonProps, isString, makeNumberProp, makeStringProp, nullableBooleanProp, truthProp } from '../_utils'
import type { InputAlignType } from '../input'
import type { TextareaAdjustKeyboardTo, TextareaAutosizeObject, TextareaConfirmType } from './type'
export const textareaProps = {
...commonProps,
/**
* @description 输入值,支持双向绑定
*/
modelValue: String,
/**
* @description 文本位置,可选值left,center,right
*/
textAlign: String as PropType<InputAlignType>,
/**
* @description textarea是否展示输入字符。须配合max-length使用
*/
limitShow: Boolean,
/**
* @description 限制最长输入字符
*/
maxLength: [String, Number],
/**
* @description textarea的高度优先级高于autosize属性 仅支持 H5
*/
rows: [String, Number],
/**
* @description 文本域自定义类名
*/
textareaClass: {
type: [String, Object, Array] as PropType<ClassType>,
default: '',
},
/**
* @description 文本域自定义样式
*/
textareaStyle: {
type: [String, Object, Array] as PropType<StyleValue>,
default: '',
},
/**
* @description 设置占位提示文字
*/
placeholder: String,
/**
* @description 指定 placeholder 的样式
*/
placeholderStyle: makeStringProp(''),
/**
* @description 指定 placeholder 的样式类
*/
placeholderClass: makeStringProp('textarea-placeholder'),
/**
* @description 只读属性
*/
readonly: Boolean,
/**
* @description 禁用属性
*/
disabled: nullableBooleanProp,
/**
* @description 是否自适应内容高度,也可传入对象
*/
autosize: {
type: [Boolean, Object] as PropType<boolean | TextareaAutosizeObject>,
default: false,
},
/**
* @description 自动获取焦点
*/
autofocus: Boolean,
/**
* @description 指定光标与键盘的距离。取textarea距离底部的距离和cursor-spacing指定的距离的最小值作为光标与键盘的距离
*/
cursorSpacing: makeNumberProp(0),
/**
* @description 指定focus时的光标位置
*/
cursor: makeNumberProp(-1),
/**
* @description 是否显示键盘上方带有”完成“按钮那一栏
*/
showConfirmBar: truthProp,
/**
* @description 光标起始位置自动聚集时有效需与selection-end搭配使用
*/
selectionStart: makeNumberProp(-1),
/**
* @description 光标结束位置自动聚集时有效需与selection-start搭配使用
*/
selectionEnd: makeNumberProp(-1),
/**
* @description 键盘弹起时,是否自动上推页面
*/
adjustPosition: truthProp,
/**
* @description focus时点击页面的时候不收起键盘
*/
holdKeyboard: Boolean,
/**
* @description 是否去掉 iOS 下的默认内边距
*/
disableDefaultPadding: Boolean,
/**
* @description 设置键盘右下角按钮的文字,可选值 `send` `search` `next` `go` `done` `return`
*/
confirmType: makeStringProp<TextareaConfirmType>('return'),
/**
* @description 点击键盘右下角按钮时是否保持键盘不收起
*/
confirmHold: Boolean,
/**
* @description 键盘对齐位置,可选值 `cursor` `bottom`
*/
adjustKeyboardTo: makeStringProp<TextareaAdjustKeyboardTo>('cursor'),
}
export type TextareaProps = ExtractPropTypes<typeof textareaProps>
export const textareaEmits = {
[BLUR_EVENT]: (evt: TextareaOnBlurEvent) => evt instanceof Object,
[FOCUS_EVENT]: (evt: TextareaOnFocusEvent) => evt instanceof Object,
[CHANGE_EVENT]: (val1?: string, val2?: string | Event) => isString(val1) && (isString(val2) || (val2 instanceof Object)),
[UPDATE_MODEL_EVENT]: (val1?: string, val2?: string | Event) => isString(val1) && (isString(val2) || (val2 instanceof Object)),
[CONFIRM_EVENT]: (evt: TextareaOnConfirmEvent) => evt instanceof Object,
[INPUT_EVENT]: (val: string, evt: TextareaOnInputEvent) => isString(val) && evt instanceof Object,
}
export type TextareaEmits = typeof textareaEmits

View File

@@ -0,0 +1,208 @@
<script setup lang="ts">
import type { TextareaConfirmType, TextareaOnBlurEvent, TextareaOnConfirmEvent, TextareaOnFocusEvent, TextareaOnInputEvent } from '@uni-helper/uni-app-types'
import type { CSSProperties } from 'vue'
import { computed, defineComponent, nextTick, toRef } from 'vue'
import { BLUR_EVENT, CHANGE_EVENT, CONFIRM_EVENT, FOCUS_EVENT, INPUT_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
import { getMainClass, isH5, isMpAlipay, pxCheck } from '../_utils'
import { useTranslate } from '../../locale'
import { useFormDisabled } from '../form/form'
import type { InputTarget } from '../input/type'
import { textareaEmits, textareaProps } from './textarea'
const props = defineProps(textareaProps)
const emit = defineEmits(textareaEmits)
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,
})
})
const textareaClasses = computed(() => {
return [props.textareaClass, {
'nut-textarea__ali': isMpAlipay,
}]
})
const textareaStyles = computed(() => {
const style: CSSProperties = {
textAlign: props.textAlign,
}
if (typeof props.autosize === 'object') {
const { minHeight, maxHeight } = props.autosize
if (minHeight != null)
style.minHeight = pxCheck(minHeight)
if (maxHeight != null)
style.maxHeight = pxCheck(maxHeight)
}
return [props.textareaStyle, style]
})
const innerMaxLength = computed(() => {
if (props.maxLength == null)
return -1
return Number(props.maxLength)
})
function updateValue(value: string, evt: any) {
if (innerMaxLength.value > 0 && value.length > innerMaxLength.value)
value = value.slice(0, innerMaxLength.value)
emit(UPDATE_MODEL_EVENT, value, evt)
emit(CHANGE_EVENT, value, evt)
}
function _onInput(evt: TextareaOnInputEvent) {
updateValue(evt.detail.value, evt)
nextTick(() => {
emit(INPUT_EVENT, innerValue.value, evt)
})
}
function handleInput(evt: any) {
if (isH5) {
const target = evt.target as InputTarget
if (!target.composing)
_onInput(evt)
}
else {
_onInput(evt)
}
}
function handleFocus(evt: TextareaOnFocusEvent) {
if (formDisabled.value || props.readonly)
return
emit(FOCUS_EVENT, evt)
}
function handleBlur(evt: TextareaOnBlurEvent) {
if (formDisabled.value || props.readonly)
return
updateValue(evt.detail.value, evt)
emit(BLUR_EVENT, evt)
}
function handleConfirm(evt: TextareaOnConfirmEvent) {
emit(CONFIRM_EVENT, evt)
}
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'))
}
}
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-textarea`
const { translate } = useTranslate(componentName)
export default defineComponent({
name: componentName,
inheritAttrs: false,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="props.customStyle">
<textarea
v-if="props.readonly"
class="nut-textarea__textarea nut-textarea__textarea__readonly"
:class="textareaClasses"
:style="textareaStyles"
:value="innerValue"
:rows="props.rows"
:disabled="true"
:show-count="false"
:placeholder="props.placeholder || translate('placeholder')"
:placeholder-style="props.placeholderStyle"
:placeholder-class="props.placeholderClass"
:auto-height="!!props.autosize"
:disable-default-padding="props.disableDefaultPadding"
/>
<textarea
v-else
class="nut-textarea__textarea"
:class="textareaClasses"
:style="textareaStyles"
:value="innerValue"
:rows="props.rows"
:disabled="formDisabled || props.readonly"
:show-count="false"
:maxlength="innerMaxLength"
:placeholder="props.placeholder || translate('placeholder')"
:placeholder-style="props.placeholderStyle"
:placeholder-class="props.placeholderClass"
:auto-focus="props.autofocus"
:auto-height="!!props.autosize"
:cursor-spacing="props.cursorSpacing"
:cursor="props.cursor"
:show-confirm-bar="props.showConfirmBar"
:selection-start="props.selectionStart"
:selection-end="props.selectionEnd"
:adjust-position="props.adjustPosition"
:hold-keyboard="props.holdKeyboard"
:disable-default-padding="props.disableDefaultPadding"
:confirm-type="props.confirmType as TextareaConfirmType"
:confirm-hold="props.confirmHold"
:adjust-keyboard-to="props.adjustKeyboardTo"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="endComposing"
@compositionstart="startComposing"
@compositionend="endComposing"
@confirm="handleConfirm"
/>
<view v-if="props.limitShow && innerMaxLength > 0" class="nut-textarea__limit">
{{ innerValue.length }}/{{ innerMaxLength }}
</view>
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,16 @@
export interface TextareaAutosizeObject {
/**
* 最大高度
*/
maxHeight?: number | string
/**
* 最小高度
*/
minHeight?: number | string
}
export const textareaConfirmType = ['send', 'search', 'next', 'go', 'done', 'return'] as const
export type TextareaConfirmType = (typeof textareaConfirmType)[number]
export const textareaAdjustKeyboardTo = ['cursor', 'bottom']
export type TextareaAdjustKeyboardTo = (typeof textareaAdjustKeyboardTo)[number]