wd-textarea.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. <template>
  2. <view :class="rootClass" :style="customStyle">
  3. <view v-if="label || $slots.label" class="wd-textarea__label" :style="labelStyle">
  4. <text v-if="isRequired && markerSide === 'before'" class="wd-textarea__required wd-textarea__required--left">*</text>
  5. <view v-if="prefixIcon || $slots.prefix" class="wd-textarea__prefix">
  6. <wd-icon v-if="prefixIcon && !$slots.prefix" custom-class="wd-textarea__icon" :name="prefixIcon" @click="onClickPrefixIcon" />
  7. <slot v-else name="prefix"></slot>
  8. </view>
  9. <view class="wd-textarea__label-inner">
  10. <text v-if="label && !$slots.label">{{ label }}</text>
  11. <slot v-else-if="$slots.label" name="label"></slot>
  12. </view>
  13. <text v-if="isRequired && markerSide === 'after'" class="wd-textarea__required">*</text>
  14. </view>
  15. <!-- 文本域 -->
  16. <view :class="`wd-textarea__value ${showClear ? 'is-suffix' : ''} ${customTextareaContainerClass} ${showWordCount ? 'is-show-limit' : ''}`">
  17. <textarea
  18. :class="`wd-textarea__inner ${customTextareaClass}`"
  19. v-model="inputValue"
  20. :show-count="false"
  21. :placeholder="placeholderValue"
  22. :disabled="disabled || readonly"
  23. :maxlength="maxlength"
  24. :focus="focused"
  25. :auto-focus="autoFocus"
  26. :placeholder-style="placeholderStyle"
  27. :placeholder-class="inputPlaceholderClass"
  28. :auto-height="autoHeight"
  29. :cursor-spacing="cursorSpacing"
  30. :fixed="fixed"
  31. :cursor="cursor"
  32. :show-confirm-bar="showConfirmBar"
  33. :selection-start="selectionStart"
  34. :selection-end="selectionEnd"
  35. :adjust-position="adjustPosition"
  36. :hold-keyboard="holdKeyboard"
  37. :confirm-type="confirmType"
  38. :confirm-hold="confirmHold"
  39. :disable-default-padding="disableDefaultPadding"
  40. :ignoreCompositionEvent="ignoreCompositionEvent"
  41. :inputmode="inputmode"
  42. @input="handleInput"
  43. @focus="handleFocus"
  44. @blur="handleBlur"
  45. @confirm="handleConfirm"
  46. @linechange="handleLineChange"
  47. @keyboardheightchange="handleKeyboardheightchange"
  48. />
  49. <view v-if="errorMessage" class="wd-textarea__error-message">{{ errorMessage }}</view>
  50. <view v-if="props.readonly" class="wd-textarea__readonly-mask" />
  51. <view class="wd-textarea__suffix">
  52. <wd-icon v-if="showClear" custom-class="wd-textarea__clear" name="error-fill" @click="handleClear" />
  53. <view v-if="showWordCount" class="wd-textarea__count">
  54. <text :class="countClass">
  55. {{ currentLength }}
  56. </text>
  57. /{{ maxlength }}
  58. </view>
  59. </view>
  60. </view>
  61. </view>
  62. </template>
  63. <script lang="ts">
  64. export default {
  65. name: 'wd-textarea',
  66. options: {
  67. virtualHost: true,
  68. addGlobalClass: true,
  69. styleIsolation: 'shared'
  70. }
  71. }
  72. </script>
  73. <script lang="ts" setup>
  74. import { computed, onBeforeMount, ref, watch, useSlots, type Slots } from 'vue'
  75. import wdIcon from '../wd-icon/wd-icon.vue'
  76. import { objToStyle, isDef, pause } from '../common/util'
  77. import { useCell } from '../composables/useCell'
  78. import { FORM_KEY, type FormItemRule } from '../wd-form/types'
  79. import { useParent } from '../composables/useParent'
  80. import { useTranslate } from '../composables/useTranslate'
  81. import { textareaProps } from './types'
  82. interface TextareaSlots extends Slots {
  83. prefix?: () => any
  84. label?: () => any
  85. }
  86. const { translate } = useTranslate('textarea')
  87. const props = defineProps(textareaProps)
  88. const emit = defineEmits([
  89. 'update:modelValue',
  90. 'clear',
  91. 'blur',
  92. 'focus',
  93. 'input',
  94. 'keyboardheightchange',
  95. 'confirm',
  96. 'linechange',
  97. 'clickprefixicon',
  98. 'click'
  99. ])
  100. const slots = useSlots() as TextareaSlots
  101. const placeholderValue = computed(() => {
  102. return isDef(props.placeholder) ? props.placeholder : translate('placeholder')
  103. })
  104. const clearing = ref<boolean>(false)
  105. const focused = ref<boolean>(false) // 控制聚焦
  106. const focusing = ref<boolean>(false) // 当前是否激活状态
  107. const inputValue = ref<string>('') // 输入框的值
  108. const cell = useCell()
  109. watch(
  110. () => props.focus,
  111. (newValue) => {
  112. focused.value = newValue
  113. },
  114. { immediate: true, deep: true }
  115. )
  116. watch(
  117. () => props.modelValue,
  118. (newValue) => {
  119. inputValue.value = isDef(newValue) ? String(newValue) : ''
  120. },
  121. { immediate: true, deep: true }
  122. )
  123. const { parent: form } = useParent(FORM_KEY)
  124. /**
  125. * 展示清空按钮
  126. */
  127. const showClear = computed(() => {
  128. const { disabled, readonly, clearable, clearTrigger } = props
  129. if (clearable && !readonly && !disabled && inputValue.value && (clearTrigger === 'always' || (props.clearTrigger === 'focus' && focusing.value))) {
  130. return true
  131. } else {
  132. return false
  133. }
  134. })
  135. /**
  136. * 展示字数统计
  137. */
  138. const showWordCount = computed(() => {
  139. const { disabled, readonly, maxlength, showWordLimit } = props
  140. return Boolean(!disabled && !readonly && isDef(maxlength) && maxlength > -1 && showWordLimit)
  141. })
  142. // 表单校验错误信息
  143. const errorMessage = computed(() => {
  144. if (form && props.prop && form.errorMessages && form.errorMessages[props.prop]) {
  145. return form.errorMessages[props.prop]
  146. } else {
  147. return ''
  148. }
  149. })
  150. // 是否展示必填
  151. const isRequired = computed(() => {
  152. let formRequired = false
  153. if (form && form.props.rules) {
  154. const rules = form.props.rules
  155. for (const key in rules) {
  156. if (Object.prototype.hasOwnProperty.call(rules, key) && key === props.prop && Array.isArray(rules[key])) {
  157. formRequired = rules[key].some((rule: FormItemRule) => rule.required)
  158. }
  159. }
  160. }
  161. return props.required || props.rules.some((rule) => rule.required) || formRequired
  162. })
  163. // 当前文本域文字长度
  164. const currentLength = computed(() => {
  165. /**
  166. * 使用Array.from处理多码元字符以获取正确的长度
  167. * @link https://github.com/Moonofweisheng/wot-design-uni/issues/933
  168. */
  169. return Array.from(String(formatValue(props.modelValue))).length
  170. })
  171. const rootClass = computed(() => {
  172. return `wd-textarea ${props.label || slots.label ? 'is-cell' : ''} ${props.center ? 'is-center' : ''} ${cell.border.value ? 'is-border' : ''} ${
  173. props.size ? 'is-' + props.size : ''
  174. } ${props.error ? 'is-error' : ''} ${props.disabled ? 'is-disabled' : ''} ${props.autoHeight ? 'is-auto-height' : ''} ${
  175. currentLength.value > 0 ? 'is-not-empty' : ''
  176. } ${props.noBorder ? 'is-no-border' : ''} ${props.customClass}`
  177. })
  178. const labelClass = computed(() => {
  179. return `wd-textarea__label ${props.customLabelClass}`
  180. })
  181. const inputPlaceholderClass = computed(() => {
  182. return `wd-textarea__placeholder ${props.placeholderClass}`
  183. })
  184. const countClass = computed(() => {
  185. return `${currentLength.value > 0 ? 'wd-textarea__count-current' : ''} ${currentLength.value > props.maxlength ? 'is-error' : ''}`
  186. })
  187. const labelStyle = computed(() => {
  188. return props.labelWidth
  189. ? objToStyle({
  190. 'min-width': props.labelWidth,
  191. 'max-width': props.labelWidth
  192. })
  193. : ''
  194. })
  195. onBeforeMount(() => {
  196. initState()
  197. })
  198. // 状态初始化
  199. function initState() {
  200. inputValue.value = formatValue(inputValue.value)
  201. emit('update:modelValue', inputValue.value)
  202. }
  203. function formatValue(value: string | number) {
  204. if (value === null || value === undefined) return ''
  205. const { maxlength, showWordLimit } = props
  206. if (showWordLimit && maxlength !== -1 && String(value).length > maxlength) {
  207. return value.toString().substring(0, maxlength)
  208. }
  209. return `${value}`
  210. }
  211. async function handleClear() {
  212. focusing.value = false
  213. inputValue.value = ''
  214. if (props.focusWhenClear) {
  215. clearing.value = true
  216. focused.value = false
  217. }
  218. await pause()
  219. if (props.focusWhenClear) {
  220. focused.value = true
  221. focusing.value = true
  222. }
  223. emit('update:modelValue', inputValue.value)
  224. emit('clear')
  225. }
  226. async function handleBlur({ detail }: any) {
  227. // 等待150毫秒,clear执行完毕
  228. await pause(150)
  229. if (clearing.value) {
  230. clearing.value = false
  231. return
  232. }
  233. focusing.value = false
  234. emit('blur', {
  235. value: inputValue.value,
  236. cursor: detail.cursor ? detail.cursor : null
  237. })
  238. }
  239. function handleFocus({ detail }: any) {
  240. focusing.value = true
  241. emit('focus', detail)
  242. }
  243. function handleInput({ detail }: any) {
  244. inputValue.value = formatValue(inputValue.value as string)
  245. emit('update:modelValue', inputValue.value)
  246. emit('input', detail)
  247. }
  248. function handleKeyboardheightchange({ detail }: any) {
  249. emit('keyboardheightchange', detail)
  250. }
  251. function handleConfirm({ detail }: any) {
  252. emit('confirm', detail)
  253. }
  254. function handleLineChange({ detail }: any) {
  255. emit('linechange', detail)
  256. }
  257. function onClickPrefixIcon() {
  258. emit('clickprefixicon')
  259. }
  260. </script>
  261. <style lang="scss" scoped>
  262. @import './index.scss';
  263. </style>
  264. <style lang="scss">
  265. @import './placeholder.scss';
  266. </style>