提交 8cd739ec 编写于 作者: richard_1015's avatar richard_1015

Merge branch 'next' of https://github.com/jdf2e/nutui into next

...@@ -487,6 +487,16 @@ ...@@ -487,6 +487,16 @@
"sort": 6, "sort": 6,
"show": true, "show": true,
"author": "Drjingfubo" "author": "Drjingfubo"
},
{
"version": "3.0.0",
"name": "TextArea",
"type": "component",
"cName": "文本域",
"desc": "文本输入",
"sort": 7,
"show": true,
"author": "gx"
} }
] ]
}, },
......
...@@ -178,9 +178,9 @@ setup() { ...@@ -178,9 +178,9 @@ setup() {
| container-id | 在 useWindow 属性为 false 的时候,自定义设置节点ID | String | `''` | | container-id | 在 useWindow 属性为 false 的时候,自定义设置节点ID | String | `''` |
| load-more-txt | “没有更多数”据展示文案 | String | `'哎呀,这里是底部了啦'` | | load-more-txt | “没有更多数”据展示文案 | String | `'哎呀,这里是底部了啦'` |
| is-open-refresh | 是否开启下拉刷新 | Boolean | `false` | | is-open-refresh | 是否开启下拉刷新 | Boolean | `false` |
| pull-icon | 下拉刷新[图标名称](#/icon) | String | `https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png` | | pull-icon | 下拉刷新[图标名称](#/icon) | String | <img src="https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png" width=40/> |
| pull-txt | 下拉刷新提示文案 | String | `松手刷新` | | pull-txt | 下拉刷新提示文案 | String | `松手刷新` |
| load-icon | 上拉加载[图标名称](#/icon) | Boolean | `https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png` | | load-icon | 上拉加载[图标名称](#/icon) | Boolean | <img src="https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png" width=40 /> |
| load-txt | 上拉加载提示文案 | String | `加载中...` | | load-txt | 上拉加载提示文案 | String | `加载中...` |
### Events ### Events
......
...@@ -57,34 +57,6 @@ ...@@ -57,34 +57,6 @@
placeholder="支持小数点的输入" placeholder="支持小数点的输入"
label="数字:" label="数字:"
/> />
<h2>文本域</h2>
<nut-input
v-model:value="state.val7"
@change="change"
:autosize="true"
type="textarea"
placeholder="文本域"
label="留言:"
/>
<nut-input
v-model:value="state.val7"
@change="change"
rows="5"
type="textarea"
placeholder="设置输入五行"
label="留言:"
/>
<h2>显示字数统计</h2>
<nut-input
v-model:value="state.val8"
@change="change"
rows="5"
:limitShow="true"
maxLength="20"
type="textarea"
placeholder="设置输入五行"
label="留言:"
/>
</div> </div>
</template> </template>
...@@ -120,6 +92,7 @@ export default createDemo({ ...@@ -120,6 +92,7 @@ export default createDemo({
const clear = (num: string | number) => { const clear = (num: string | number) => {
console.log('clear:', num); console.log('clear:', num);
}; };
return { return {
state, state,
change, change,
......
...@@ -20,8 +20,20 @@ app.use(input); ...@@ -20,8 +20,20 @@ app.use(input);
双向绑定 双向绑定
```html ```html
<nut-input v-model:value="state.val1" @change="change" label="标题:" /> <nut-input
v-model:value="state.val1"
@change="change"
@focus="focus"
@blur="blur"
label="文本"
/>
<nut-input placeholder="请输入文本"
@change="change"
v-model:value="state.val0"
:requireShow="true"
label="文本"
@clear="clear"
/>
``` ```
### 禁用和只读 ### 禁用和只读
...@@ -44,19 +56,7 @@ app.use(input); ...@@ -44,19 +56,7 @@ app.use(input);
<nut-input v-model:value="state.val5" @change="change" type="digit" label="整数:" /> <nut-input v-model:value="state.val5" @change="change" type="digit" label="整数:" />
<nut-input v-model:value="state.val6" @change="change" type="digit" placeholder="支持小数点的输入" label="数字:"/> <nut-input v-model:value="state.val6" @change="change" type="digit" placeholder="支持小数点的输入" label="数字:"/>
``` ```
### 文本域
```html
<nut-input v-model:value="state.val7" @change="change" autosize="true" type="textarea" placeholder="文本域" label="留言:"/>
<nut-input v-model:value="state.val7" @change="change" rows="5" type="textarea" placeholder="设置输入五行" label="留言:"/>
```
### 文本域字数统计
```html
<nut-input v-model:value="state.val8" @change="change" rows="5" limitShow="true" maxLength="20" type="textarea" placeholder="设置输入五行" label="留言:"/>
```
| 参数 | 说明 | 类型 | 默认值 | | 参数 | 说明 | 类型 | 默认值 |
...@@ -64,15 +64,13 @@ app.use(input); ...@@ -64,15 +64,13 @@ app.use(input);
| type | 类型,可选值为 `text` `textarea` `number` 等 | String |`text` | | type | 类型,可选值为 `text` `textarea` `number` 等 | String |`text` |
| value | 输入值,双向绑定 | String | - | | value | 输入值,双向绑定 | String | - |
| placeholder | 为空时占位符 | String | - | | placeholder | 为空时占位符 | String | - |
| placeholder-style | placeholder 样式 | String | - |
| label | 左侧文案 | string | - | | label | 左侧文案 | string | - |
| requireShow |左侧*号是否展示 | boolean | `false` |
| disabled | 是否禁用 | boolean | `false` | | disabled | 是否禁用 | boolean | `false` |
| readonly | 是否只读 | boolean | `false` | | readonly | 是否只读 | boolean | `false` |
| clear-btn | 是否带清除按钮(icon) | boolean | `true` |
| required | 是否带必填的*号,且blur事件做非空校验 | boolean | `false` |
| maxlength | 限制最长输入字符 | string/number | - | | maxlength | 限制最长输入字符 | string/number | - |
| rows | textarea时高度 | string/number | 2 | | disableClear | 禁止展示清除icon | boolean | false |
| limit-show | textarea时是否展示输入字符。须设置maxlength | boolean | `false` | | textAlign | 文本位置 | string | `left` |
| change | 输入内容时触发 | function | - | | change | 输入内容时触发 | function | - |
| focus | 聚焦时触发 | function | - | | focus | 聚焦时触发 | function | - |
| blur | 失焦时触发 | function | - | | blur | 失焦时触发 | function | - |
......
...@@ -28,29 +28,6 @@ ...@@ -28,29 +28,6 @@
position: absolute; position: absolute;
right: 15px; right: 15px;
} }
.nut-text {
flex: 1;
padding: 0 10px;
.nut-text-limit {
float: right;
color: rgba(153, 153, 153, 1);
}
.nut-text-core {
outline: none;
display: block;
box-sizing: border-box;
width: 100%;
min-width: 0;
margin: 0;
padding: 0;
color: #323233;
line-height: inherit;
text-align: left;
background-color: transparent;
border: 0;
resize: none;
}
}
} }
.nut-input-disabled { .nut-input-disabled {
color: #c8c9cc !important; color: #c8c9cc !important;
......
<template> <template>
<view :class="['nut-input', { 'nut-input-disabled': disabled }]"> <view :class="classes">
<view class="nut-input-label"> <view class="nut-input-label">
<view class="nut-input-require" v-if="requireShow">*</view> <view class="nut-input-require" v-if="requireShow">*</view>
<view v-if="label" class="label-string">{{ label }}</view> <view v-if="label" class="label-string">{{ label }}</view>
</view> </view>
<view v-if="type === 'textarea'" class="nut-text">
<textarea
:style="styles"
:rows="rows"
@input="valueChange"
v-model="state.curretvalue"
class="nut-text-core"
:maxlength="maxLength"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
>
</textarea>
<span class="nut-text-limit" v-if="limitShow">
<span :class="[{ 'nut-field-over': state.textNum > maxLength }]">{{
state.textNum
}}</span>
<span>/{{ maxLength }}</span>
</span>
</view>
<input <input
v-else
class="input-text" class="input-text"
:style="styles" :style="styles"
:type="type" :type="type"
...@@ -43,7 +21,7 @@ ...@@ -43,7 +21,7 @@
@click="handleClear" @click="handleClear"
class="nut-textinput-clear" class="nut-textinput-clear"
v-if="!disableClear && !readonly" v-if="!disableClear && !readonly"
v-show="type !== 'textarea' && active" v-show="active && state.curretvalue.length > 0"
> >
<nut-icon name="close-little" size="12px"></nut-icon> <nut-icon name="close-little" size="12px"></nut-icon>
</view> </view>
...@@ -54,43 +32,30 @@ import { ref, toRefs, reactive, computed } from 'vue'; ...@@ -54,43 +32,30 @@ import { ref, toRefs, reactive, computed } from 'vue';
import { createComponent } from '@/utils/create'; import { createComponent } from '@/utils/create';
import { formatNumber } from './util'; import { formatNumber } from './util';
const { create } = createComponent('input'); const { componentName, create } = createComponent('input');
interface Events {
eventName: 'change' | 'focus' | 'blur' | 'clear' | 'update:value';
params: (string | number | Event)[];
}
export default create({ export default create({
props: { props: {
type: { type: {
type: String, type: String,
default: 'text' default: 'text'
}, },
textAlign: { value: {
type: String, type: [String, Number],
default: 'left'
},
limitShow: {
type: Boolean,
default: false
},
maxLength: {
type: String,
default: '' default: ''
}, },
requireShow: { placeholder: {
type: Boolean,
default: false
},
rows: {
type: String, type: String,
default: '' default: '请输入信息'
}, },
label: { label: {
type: String, type: String,
default: '' default: ''
}, },
placeholder: { requireShow: {
type: String,
default: '请输入信息'
},
readonly: {
type: Boolean, type: Boolean,
default: false default: false
}, },
...@@ -98,12 +63,16 @@ export default create({ ...@@ -98,12 +63,16 @@ export default create({
type: Boolean, type: Boolean,
default: false default: false
}, },
autosize: { readonly: {
type: Boolean, type: Boolean,
default: false default: false
}, },
value: { textAlign: {
type: [String, Number], type: String,
default: 'left'
},
maxLength: {
type: String,
default: '' default: ''
}, },
disableClear: { disableClear: {
...@@ -115,25 +84,13 @@ export default create({ ...@@ -115,25 +84,13 @@ export default create({
emits: ['change', 'update:value', 'blur', 'focus', 'clear', 'error'], emits: ['change', 'update:value', 'blur', 'focus', 'clear', 'error'],
setup(props, { emit }) { setup(props, { emit }) {
interface Events {
eventName:
| 'change'
| 'focus'
| 'blur'
| 'clear'
| 'update:value'
| 'error';
params: (string | number | Event)[];
}
const { const {
label, label,
placeholder, placeholder,
disabled, disabled,
readonly, readonly,
requireShow, requireShow,
maxLength, maxLength
rows
} = props; } = props;
const { value } = toRefs(props); const { value } = toRefs(props);
const active = ref(false); const active = ref(false);
...@@ -141,14 +98,15 @@ export default create({ ...@@ -141,14 +98,15 @@ export default create({
curretvalue: value, curretvalue: value,
textNum: String(value.value).length textNum: String(value.value).length
}); });
const classes = computed(() => {
return {
[componentName]: true,
'nut-input-disabled': disabled
};
});
const styles = computed(() => { const styles = computed(() => {
const rize =
props.type == 'textarea'
? `'resize':${props.autosize ? 'none' : 'horizontal'}`
: '';
return { return {
'text-align': props.textAlign, 'text-align': props.textAlign
rize
}; };
}); });
const emitChange = (envs: Array<Events>) => { const emitChange = (envs: Array<Events>) => {
...@@ -162,12 +120,6 @@ export default create({ ...@@ -162,12 +120,6 @@ export default create({
if (maxLength && val.length > Number(maxLength)) { if (maxLength && val.length > Number(maxLength)) {
val = val.slice(0, Number(maxLength)); val = val.slice(0, Number(maxLength));
emitChange([
{
eventName: 'error',
params: [val]
}
]);
} }
if (props.type == 'digit') { if (props.type == 'digit') {
val = formatNumber(val, true); val = formatNumber(val, true);
...@@ -176,8 +128,6 @@ export default create({ ...@@ -176,8 +128,6 @@ export default create({
val = formatNumber(val, false); val = formatNumber(val, false);
} }
state.textNum = val.length; state.textNum = val.length;
// input.value = val;
//state.curretvalue = val;
emitChange([ emitChange([
{ {
eventName: 'update:value', eventName: 'update:value',
...@@ -243,7 +193,6 @@ export default create({ ...@@ -243,7 +193,6 @@ export default create({
placeholder, placeholder,
label, label,
disabled, disabled,
rows,
state, state,
styles, styles,
active, active,
...@@ -252,6 +201,7 @@ export default create({ ...@@ -252,6 +201,7 @@ export default create({
valueFocus, valueFocus,
valueBlur, valueBlur,
handleClear, handleClear,
classes,
emitChange emitChange
}; };
} }
......
...@@ -12,17 +12,23 @@ ...@@ -12,17 +12,23 @@
<view-block <view-block
class="nut-picker__item" class="nut-picker__item"
:key="index" :key="index"
v-for="(item, index) in state.options" v-for="(item, index) in options"
>{{ dataType === 'cascade' ? item.text : item }}</view-block >{{ dataType === 'cascade' ? item.text : item }}</view-block
> >
</view-block> </view-block>
</view-block> </view-block>
</template> </template>
<script lang="ts"> <script lang="ts">
import { reactive, ref, watch, computed } from 'vue'; import { reactive, ref, watch, computed, toRefs, onMounted } from 'vue';
import { createComponent } from '@/utils/create'; import { createComponent } from '@/utils/create';
import { useTouch } from '@/utils/useTouch'; import { useTouch } from '@/utils/useTouch';
import { commonProps } from './commonProps'; import { commonProps } from './commonProps';
import {
PickerObjOpt,
PickerOption,
PickerObjectColumn,
PickerObjectColumns
} from './types';
const MOMENTUM_LIMIT_DISTANCE = 15; const MOMENTUM_LIMIT_DISTANCE = 15;
const MOMENTUM_LIMIT_TIME = 300; const MOMENTUM_LIMIT_TIME = 300;
const DEFAULT_DURATION = 200; const DEFAULT_DURATION = 200;
...@@ -34,7 +40,6 @@ function stopPropagation(event: Event) { ...@@ -34,7 +40,6 @@ function stopPropagation(event: Event) {
event.stopPropagation(); event.stopPropagation();
} }
function preventDefault(event: Event, isStopPropagation?: boolean) { function preventDefault(event: Event, isStopPropagation?: boolean) {
/* istanbul ignore else */
if (typeof event.cancelable !== 'boolean' || event.cancelable) { if (typeof event.cancelable !== 'boolean' || event.cancelable) {
event.preventDefault(); event.preventDefault();
} }
...@@ -44,7 +49,7 @@ function preventDefault(event: Event, isStopPropagation?: boolean) { ...@@ -44,7 +49,7 @@ function preventDefault(event: Event, isStopPropagation?: boolean) {
} }
} }
function getElementTranslateY(element) { function getElementTranslateY(element: Element) {
const style = window.getComputedStyle(element); const style = window.getComputedStyle(element);
const transform = style.transform || style.webkitTransform; const transform = style.transform || style.webkitTransform;
const translateY = transform.slice(7, transform.length - 1).split(', ')[5]; const translateY = transform.slice(7, transform.length - 1).split(', ')[5];
...@@ -54,7 +59,7 @@ export function isObject(val: unknown): val is Record<any, any> { ...@@ -54,7 +59,7 @@ export function isObject(val: unknown): val is Record<any, any> {
return val !== null && typeof val === 'object'; return val !== null && typeof val === 'object';
} }
function isOptionDisabled(option) { function isOptionDisabled(option: PickerObjectColumn) {
return isObject(option) && option.disabled; return isObject(option) && option.disabled;
} }
...@@ -66,46 +71,57 @@ export default create({ ...@@ -66,46 +71,57 @@ export default create({
emits: ['click', 'change'], emits: ['click', 'change'],
setup(props, { emit }) { setup(props, { emit }) {
let moving; const wrapper = ref();
let startOffset, touchStartTime, momentumOffset, transitionEndTrigger;
const state = reactive({ const state = reactive({
index: props.defaultIndex, index: props.defaultIndex,
offset: 0, offset: 0,
duration: 0, duration: 0,
options: props.listData options: props.listData as PickerObjectColumn[],
moving: false,
startOffset: 0,
touchStartTime: 0,
momentumOffset: 0,
transitionEndTrigger: null as null | Function
}); });
watch(
() => props.listData,
val => {
if (val) {
state.options = val;
}
}
);
const wrapper = ref();
const touch = useTouch(); const touch = useTouch();
const count = () => state.options.length;
const _show = ref(false);
const getIndexByOffset = offset =>
range(Math.round(-offset / props.itemHeight), 0, count() - 1);
const baseOffset = () => const wrapperStyle = computed(() => ({
(props.itemHeight * (props.visibleItemCount - 1)) / 2; transform: `translate3d(0, ${state.offset + baseOffset()}px, 0)`,
transitionDuration: `${state.duration}ms`,
transitionProperty: state.duration ? 'all' : 'none'
}));
const handleClick = (event: Event) => {
emit('click', event);
};
const getIndexByOffset = (offset: number) => {
return range(
Math.round(-offset / +props.itemHeight),
0,
state.options.length - 1
);
};
const baseOffset = () => {
return (+props.itemHeight * (+props.visibleItemCount - 1)) / 2;
};
const stopMomentum = () => { const stopMomentum = () => {
moving = false; state.moving = false;
state.duration = 0; state.duration = 0;
if (transitionEndTrigger) { if (state.transitionEndTrigger) {
transitionEndTrigger(); state.transitionEndTrigger();
transitionEndTrigger = null; state.transitionEndTrigger = null;
} }
}; };
const adjustIndex = index => { const adjustIndex = (index: number) => {
index = range(index, 0, count()); index = range(index, 0, state.options.length);
for (let i = index; i < count(); i++) { for (let i = index; i < state.options.length; i++) {
if (!isOptionDisabled(state.options[i])) return i; if (!isOptionDisabled(state.options[i])) return i;
} }
for (let i = index - 1; i >= 0; i--) { for (let i = index - 1; i >= 0; i--) {
...@@ -113,10 +129,10 @@ export default create({ ...@@ -113,10 +129,10 @@ export default create({
} }
}; };
const setIndex = (index, emitChange = false) => { const setIndex = (index: number, emitChange = false) => {
index = adjustIndex(index) || 0; index = adjustIndex(index) || 0;
const offset = -index * props.itemHeight; const offset = -index * +props.itemHeight;
const trigger = () => { const trigger = () => {
if (index !== state.index) { if (index !== state.index) {
state.index = index; state.index = index;
...@@ -127,22 +143,16 @@ export default create({ ...@@ -127,22 +143,16 @@ export default create({
} }
}; };
if (moving && offset !== state.offset) { if (state.moving && offset !== state.offset) {
transitionEndTrigger = trigger; state.transitionEndTrigger = trigger;
} else { } else {
trigger(); trigger();
} }
state.offset = offset; state.offset = offset;
}; };
watch(
() => props.defaultIndex, const momentum = (distance: number, duration: number) => {
val => {
setIndex(val);
}
);
setIndex(props.defaultIndex);
const momentum = (distance, duration) => {
const speed = Math.abs(distance / duration); const speed = Math.abs(distance / duration);
distance = state.offset + (speed / 0.003) * (distance < 0 ? -1 : 1); distance = state.offset + (speed / 0.003) * (distance < 0 ? -1 : 1);
...@@ -151,57 +161,58 @@ export default create({ ...@@ -151,57 +161,58 @@ export default create({
setIndex(index, true); setIndex(index, true);
}; };
const onTouchStart = event => {
const onTouchStart = (event: Event) => {
if (props.readonly) { if (props.readonly) {
return; return;
} }
touch.start(event); touch.start(event);
if (moving) { if (state.moving) {
const translateY = getElementTranslateY(wrapper.value); const translateY = getElementTranslateY(wrapper.value);
state.offset = Math.min(0, translateY - baseOffset()); state.offset = Math.min(0, translateY - baseOffset());
startOffset = state.offset; state.startOffset = state.offset;
} else { } else {
startOffset = state.offset; state.startOffset = state.offset;
} }
state.duration = 0; state.duration = 0;
touchStartTime = Date.now(); state.touchStartTime = Date.now();
momentumOffset = startOffset; state.momentumOffset = state.startOffset;
transitionEndTrigger = null; state.transitionEndTrigger = null;
}; };
const onTouchMove = event => { const onTouchMove = (event: Event) => {
if (props.readonly) { if (props.readonly) {
return; return;
} }
moving = true; state.moving = true;
touch.move(event); touch.move(event);
if (touch.isVertical()) { if (touch.isVertical()) {
moving = true; state.moving = true;
preventDefault(event, true); preventDefault(event, true);
} }
const moveOffset = startOffset + touch.deltaY.value; const moveOffset = state.startOffset + touch.deltaY.value;
if (moveOffset > props.itemHeight) { if (moveOffset > props.itemHeight) {
state.offset = props.itemHeight; state.offset = props.itemHeight as number;
} else { } else {
state.offset = startOffset + touch.deltaY.value; state.offset = state.startOffset + touch.deltaY.value;
} }
const now = Date.now(); const now = Date.now();
if (now - touchStartTime > MOMENTUM_LIMIT_TIME) { if (now - state.touchStartTime > MOMENTUM_LIMIT_TIME) {
touchStartTime = now; state.touchStartTime = now;
momentumOffset = state.offset; state.momentumOffset = state.offset;
} }
}; };
const onTouchEnd = () => { const onTouchEnd = () => {
const index = getIndexByOffset(state.offset); const index = getIndexByOffset(state.offset);
state.duration = DEFAULT_DURATION; state.duration = DEFAULT_DURATION;
setIndex(index, true); setIndex(index, true);
const distance = state.offset - momentumOffset; const distance = state.offset - state.momentumOffset;
const duration = Date.now() - touchStartTime; const duration = Date.now() - state.touchStartTime;
const allowMomentum = const allowMomentum =
duration < MOMENTUM_LIMIT_TIME && duration < MOMENTUM_LIMIT_TIME &&
...@@ -212,25 +223,37 @@ export default create({ ...@@ -212,25 +223,37 @@ export default create({
return; return;
} }
}; };
const handleClick = (event: Event) => {
emit('click', event); onMounted(() => {
}; setIndex(+props.defaultIndex);
const wrapperStyle = computed(() => ({ });
transform: `translate3d(0, ${state.offset + baseOffset()}px, 0)`,
transitionDuration: `${state.duration}ms`, watch(
transitionProperty: state.duration ? 'all' : 'none' () => props.listData,
})); val => {
if (val) {
state.options = val as PickerObjectColumn[];
}
}
);
watch(
() => props.defaultIndex,
val => {
setIndex(+val);
}
);
return { return {
...toRefs(state),
wrapper, wrapper,
onTouchStart, onTouchStart,
onTouchMove, onTouchMove,
onTouchEnd, onTouchEnd,
wrapperStyle, wrapperStyle,
state,
stopMomentum, stopMomentum,
columns: state.options, columns: state.options,
height: Number(props.visibleItemCount) * props.itemHeight height: Number(props.visibleItemCount) * +props.itemHeight
}; };
} }
}); });
......
export const commonProps = { export const commonProps = {
listData: { listData: {
type: Array, type: Array,
default: [] default: () => {
return [];
}
}, },
readonly: { readonly: {
type: Boolean, type: Boolean,
default: false default: false
}, },
visibleItemCount: { visibleItemCount: {
type: [Number], type: [Number, String],
default: 7 default: 7
}, },
defaultIndex: { defaultIndex: {
...@@ -16,7 +18,7 @@ export const commonProps = { ...@@ -16,7 +18,7 @@ export const commonProps = {
default: 0 default: 0
}, },
itemHeight: { itemHeight: {
type: [Number], type: [Number, String],
default: 35 default: 35
} }
}; };
...@@ -95,8 +95,8 @@ export default createDemo({ ...@@ -95,8 +95,8 @@ export default createDemo({
}` }`
); );
const desc3 = ref( const desc3 = ref(
`${listData3[0].text} `${listData3[0].text}
${listData3[0].children[0].text} ${listData3[0].children[0].text}
${listData3[0].children[0].children[0].text}` ${listData3[0].children[0].children[0].text}`
); );
const descList = [desc, desc2, desc3]; const descList = [desc, desc2, desc3];
...@@ -110,16 +110,16 @@ export default createDemo({ ...@@ -110,16 +110,16 @@ export default createDemo({
desc, desc,
desc2, desc2,
desc3, desc3,
open: index => { open: (index: number) => {
showList[index - 1].value = true; showList[index - 1].value = true;
}, },
confirm: res => { confirm: (res: any) => {
desc.value = res; desc.value = res;
}, },
confirm2: res => { confirm2: (res: any) => {
desc2.value = res.join(' '); desc2.value = res.join(' ');
}, },
confirm3: res => { confirm3: (res: any) => {
desc3.value = res.join(' '); desc3.value = res.join(' ');
} }
}; };
......
# picker组件 # Picker组件
### 介绍 ### 介绍
......
<template> <template>
<view-block class="nut-picker"> <view-block :class="classes">
<nut-popup <nut-popup
position="bottom" position="bottom"
:style="{ height: height + 56 + 'px' }" :style="{ height: height + 56 + 'px' }"
...@@ -7,9 +7,9 @@ ...@@ -7,9 +7,9 @@
@close="close" @close="close"
> >
<view-block class="nut-picker__bar"> <view-block class="nut-picker__bar">
<view-block class="nut-picker__left" @click="close()"> 取消</view-block> <view-block class="nut-picker__left" @click="close">取消</view-block>
<view-block> {{ title }}</view-block> <view-block> {{ title }}</view-block>
<view-block @click="confirm()"> 确定</view-block> <view-block @click="confirm()">确定</view-block>
</view-block> </view-block>
<view-block class="nut-picker__column"> <view-block class="nut-picker__column">
...@@ -45,12 +45,18 @@ ...@@ -45,12 +45,18 @@
</view-block> </view-block>
</template> </template>
<script lang="ts"> <script lang="ts">
import { reactive, ref, watch, computed, toRaw } from 'vue'; import { reactive, watch, computed, toRaw, toRefs } from 'vue';
import { createComponent } from '@/utils/create'; import { createComponent } from '@/utils/create';
import column from './Column.vue'; import column from './Column.vue';
import popup from '@/packages/popup/index.vue'; import popup from '@/packages/popup/index.vue';
import { commonProps } from './commonProps'; import { commonProps } from './commonProps';
const { create } = createComponent('picker'); import {
PickerObjOpt,
PickerOption,
PickerObjectColumn,
PickerObjectColumns
} from './types';
const { create, componentName } = createComponent('picker');
export default create({ export default create({
children: [column, popup], children: [column, popup],
...@@ -65,61 +71,82 @@ export default create({ ...@@ -65,61 +71,82 @@ export default create({
}, },
...commonProps ...commonProps
}, },
components: { column },
emits: ['close', 'change', 'confirm', 'update:isVisible'], emits: ['close', 'change', 'confirm', 'update:isVisible'],
setup(props, { emit }) { setup(props, { emit }) {
const show = ref(false);
const defaultIndex = ref(props.defaultIndex);
const formattedColumns: any = ref(props.listData);
//临时变量,当点击确定时候赋值
let _defaultIndex = props.defaultIndex;
const childrenKey = 'children'; const childrenKey = 'children';
const valuesKey = 'values'; const valuesKey = 'values';
const state = reactive({
show: false,
formattedColumns: props.listData as PickerObjectColumn[],
defaultIndex: props.defaultIndex as number
});
//临时变量,当点击确定时候赋值
let _defaultIndex = props.defaultIndex;
let defaultIndexList: number[] = []; let defaultIndexList: number[] = [];
watch( const classes = computed(() => {
() => props.isVisible, const prefixCls = componentName;
val => { return {
show.value = val; [prefixCls]: true
} };
); });
watch( const top = computed(() => {
() => props.listData, return (Number(+props.visibleItemCount - 1) / 2) * +props.itemHeight;
val => { });
formattedColumns.value = val;
}
);
const addDefaultIndexList = listData => { const height = computed(() => {
defaultIndexList = []; return Number(props.visibleItemCount) * +props.itemHeight;
listData.forEach(res => { });
defaultIndexList.push(res.defaultIndex);
});
};
const dataType = computed(() => {
const firstColumn = formattedColumns.value[0] || {};
const dataType = computed(() => {
const firstColumn = state.formattedColumns[0] as PickerObjectColumn;
if (typeof firstColumn === 'object') { if (typeof firstColumn === 'object') {
if (firstColumn?.[childrenKey]) { if (firstColumn[childrenKey]) {
return 'cascade'; return 'cascade';
} else if (firstColumn?.[valuesKey]) { } else if (firstColumn?.[valuesKey]) {
addDefaultIndexList(props.listData); addDefaultIndexList(props.listData as PickerObjectColumn[]);
//多列
return 'multipleColumns'; return 'multipleColumns';
} }
} }
return 'text'; return 'text';
}); });
const formatCascade = (listData, defaultIndex) => {
const formatted: any[] = []; const columnList = computed(() => {
let children = listData; if (dataType.value === 'text') {
return [
{ values: state.formattedColumns, defaultIndex: state.defaultIndex }
];
} else if (dataType.value === 'multipleColumns') {
return state.formattedColumns;
} else if (dataType.value === 'cascade') {
return formatCascade(
state.formattedColumns as PickerObjectColumn[],
state.defaultIndex
);
}
return state.formattedColumns;
});
const addDefaultIndexList = (listData: PickerObjectColumn[]) => {
defaultIndexList = [];
listData.forEach(res => {
defaultIndexList.push(res.defaultIndex as number);
});
};
const formatCascade = (
listData: PickerObjectColumn[],
defaultIndex: number
) => {
const formatted: PickerObjectColumn[] = [];
let children = listData as PickerObjectColumns;
children.defaultIndex = defaultIndex; children.defaultIndex = defaultIndex;
while (children) { while (children) {
formatted.push({ formatted.push({
values: children, values: children,
defaultIndex: children.defaultIndex defaultIndex: defaultIndex
}); });
children = children?.[children.defaultIndex || 0].children; children = children?.[children.defaultIndex || 0].children;
} }
...@@ -127,94 +154,104 @@ export default create({ ...@@ -127,94 +154,104 @@ export default create({
return formatted; return formatted;
}; };
const columnList = computed(() => { const getCascadeData = (
if (dataType.value === 'text') { listData: PickerObjectColumn[],
return [ defaultIndex: number
{ values: formattedColumns.value, defaultIndex: defaultIndex.value } ) => {
]; let arr = listData as PickerObjectColumns;
} else if (dataType.value === 'multipleColumns') {
return formattedColumns.value;
} else if (dataType.value === 'cascade') {
return formatCascade(formattedColumns.value, defaultIndex.value);
}
return formattedColumns.value;
});
const getCascadeData = (listData, defaultIndex) => {
let arr = listData;
arr.defaultIndex = defaultIndex; arr.defaultIndex = defaultIndex;
const dataList: string[] = []; const dataList: string[] = [];
while (arr) { while (arr) {
const item = arr[arr.defaultIndex ?? 0]; const item = arr[arr.defaultIndex ?? 0];
dataList.push(item.text); dataList.push(item.text as string);
arr = item.children; arr = item.children;
} }
return dataList; return dataList;
}; };
return {
show, const close = () => {
column, emit('close');
title: props.title, emit('update:isVisible', false);
dataType, };
columnList,
top: (Number(props.visibleItemCount - 1) / 2) * props.itemHeight, const changeHandler = (columnIndex: number, dataIndex: number) => {
height: Number(props.visibleItemCount) * props.itemHeight, if (dataType.value === 'cascade') {
close: () => { let cursor = toRaw(state.formattedColumns) as PickerObjectColumns;
emit('close'); if (columnIndex === 0) {
emit('update:isVisible', false); state.defaultIndex = dataIndex;
}, } else {
changeHandler: (columnIndex, dataIndex) => { let i = 0;
if (dataType.value === 'cascade') { while (cursor) {
let cursor: any = toRaw(formattedColumns.value); if (i === columnIndex) {
//最外层使用props.defaultIndex作为初始index cursor.defaultIndex = dataIndex;
if (columnIndex === 0) { } else if (i > columnIndex) {
defaultIndex.value = dataIndex; cursor.defaultIndex = 0;
} else {
let i = 0;
while (cursor) {
if (i === columnIndex) {
cursor.defaultIndex = dataIndex;
} else if (i > columnIndex) {
cursor.defaultIndex = 0;
}
cursor = cursor[cursor.defaultIndex || 0].children;
i++;
} }
cursor = cursor[cursor.defaultIndex || 0].children;
i++;
} }
} else if (dataType.value === 'text') {
_defaultIndex = dataIndex;
} else if (dataType.value === 'multipleColumns') {
defaultIndexList[columnIndex] = dataIndex;
const val = defaultIndexList.map(
(res, i) => toRaw(formattedColumns.value)[i].values[res]
);
console.log('val', defaultIndexList);
emit('change', val);
} }
}, } else if (dataType.value === 'text') {
_defaultIndex = dataIndex;
confirm: () => { } else if (dataType.value === 'multipleColumns') {
if (dataType.value === 'text') { defaultIndexList[columnIndex] = dataIndex;
defaultIndex.value = _defaultIndex; const val = defaultIndexList.map(
emit('confirm', formattedColumns.value[_defaultIndex]); (res, i) =>
} else if (dataType.value === 'multipleColumns') { toRaw(state.formattedColumns as PickerObjectColumns)[i].values[res]
for (let i = 0; i < defaultIndexList.length; i++) { );
formattedColumns.value[i].defaultIndex = defaultIndexList[i]; emit('change', val);
} }
const checkedArr = toRaw(formattedColumns.value).map( };
(res: any) => res.values[res.defaultIndex]
); const confirm = () => {
console.log(formattedColumns.value); if (dataType.value === 'text') {
emit('confirm', checkedArr); state.defaultIndex = _defaultIndex as number;
} else if (dataType.value === 'cascade') { emit('confirm', state.formattedColumns[_defaultIndex as number]);
emit( } else if (dataType.value === 'multipleColumns') {
'confirm', for (let i = 0; i < defaultIndexList.length; i++) {
getCascadeData(toRaw(formattedColumns.value), defaultIndex.value) state.formattedColumns[i].defaultIndex = defaultIndexList[i];
);
} }
const checkedArr = toRaw(state.formattedColumns).map(
(res: PickerObjectColumn) =>
res.values && res.values[res.defaultIndex as number]
);
emit('confirm', checkedArr);
} else if (dataType.value === 'cascade') {
emit(
'confirm',
getCascadeData(toRaw(state.formattedColumns), state.defaultIndex)
);
}
emit('update:isVisible', false);
};
watch(
() => props.isVisible,
val => {
state.show = val;
}
);
emit('update:isVisible', false); watch(
() => props.listData,
val => {
state.formattedColumns = val as PickerObjectColumns;
} }
);
return {
classes,
...toRefs(state),
column,
dataType,
columnList,
top,
height,
close,
changeHandler,
confirm
}; };
} }
}); });
......
export type PickerObjOpt = {
text?: string;
[key: string]: any;
};
export type PickerOption = string | PickerObjOpt;
export type PickerObjectColumn = {
values?: PickerOption[];
defaultIndex?: number;
children?: PickerOption[];
[key: string]: any;
};
export type PickerObjectColumns = PickerObjectColumn & PickerObjOpt[];
<template>
<div class="demo-nopading">
<h2>基础用法</h2>
<nut-textarea
v-model:value="state.val0"
@change="change"
rows="5"
placeholder="高度可拉伸"
:autosize="true"
label="留言:"
/>
<h2>显示字数统计</h2>
<nut-textarea
v-model:value="state.val1"
@change="change"
rows="5"
:limitShow="true"
maxLength="20"
type="textarea"
placeholder="设置输入五行"
label="留言:"
/>
</div>
</template>
<script lang="ts">
import { reactive } from 'vue';
import { createComponent } from '@/utils/create';
const { createDemo } = createComponent('textarea');
export default createDemo({
setup() {
const state = reactive({
val0: '',
val1: '初始数据'
});
setTimeout(function() {
state.val1 = '异步测试数据,2秒';
}, 2000);
const change = (num: string | number) => {
console.log('change: ', num);
};
const focus = (num: string | number) => {
console.log('focus:', num);
};
const blur = (num: string | number) => {
console.log('blur:', num);
};
const clear = (num: string | number) => {
console.log('clear:', num);
};
return {
state,
change,
blur,
clear,
focus
};
}
});
</script>
<style lang="scss" scoped>
.demo-nopading {
height: 100%;
background: #f7f8fa;
overflow-x: hidden;
overflow-y: auto;
padding: 0;
padding-top: 57px;
h2 {
padding-left: 25px;
margin-top: 25px;
margin-bottom: 10px;
color: #909ca4;
}
}
</style>
# Input 输入框组件
### 介绍
### 安装
``` javascript
import { createApp } from 'vue';
import { input } from '@nutui/nutui';
const app = createApp();
app.use(input);
```
## 代码演示
### 基础用法
```html
<nut-textarea
v-model:value="state.val0"
@change="change"
rows="5"
placeholder="高度可拉伸"
:autosize="true"
label="留言:"
/>
```
### 显示字数统计
```html
<nut-textarea
v-model:value="state.val1"
@change="change"
rows="5"
:limitShow="true"
maxLength="20"
type="textarea"
placeholder="设置输入五行"
label="留言:"
/>
```
| 参数 | 说明 | 类型 | 默认值 |
|--------------|----------------------------------|--------|------------------|
| value | 输入值,双向绑定 | String | - |
| placeholder | 为空时占位符 | String | - |
| label | 左侧文案 | string | - |
| maxlength | 限制最长输入字符 | string/number | - |
| rows | textarea时高度 | string/number | 2 |
| limit-show | textarea时是否展示输入字符。须设置maxlength | boolean | `false` |
| change | 输入内容时触发 | function | - |
| focus | 聚焦时触发 | function | - |
| blur | 失焦时触发 | function | - |
| clear | 点击清空时触发 | function | - |
.nut-textarea {
position: relative;
width: 100%;
padding: 10px 0px 10px 25px;
display: flex;
background: rgba(255, 255, 255, 1);
border-bottom: 1px solid rgba(234, 240, 251, 1);
font-size: 14px;
input {
width: 230px;
flex: 1;
padding: 0 10px;
}
.nut-input-label {
width: 80px;
overflow: hidden;
display: inline-block;
text-align: left;
.label-string {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.nut-textinput-clear {
width: 16px;
height: 16px;
position: absolute;
right: 15px;
}
.nut-text {
flex: 1;
padding: 0 10px;
.nut-text-limit {
float: right;
color: rgba(153, 153, 153, 1);
}
.nut-text-core {
outline: none;
display: block;
box-sizing: border-box;
width: 100%;
min-width: 0;
margin: 0;
padding: 0;
color: #323233;
line-height: inherit;
text-align: left;
background-color: transparent;
border: 0;
resize: none;
}
}
}
<template>
<view class="nut-textarea">
<view class="nut-input-label">
<view v-if="props.label" class="label-string">{{ props.label }}</view>
</view>
<view class="nut-text">
<textarea
:style="styles"
:rows="props.rows"
@input="valueChange"
v-model="state.curretvalue"
class="nut-text-core"
:maxlength="maxLength"
:placeholder="props.placeholder"
:disabled="props.disabled"
:readonly="props.readonly"
>
</textarea>
<view class="nut-text-limit" v-if="limitShow">
<view :class="[{ 'nut-field-over': state.textNum > maxLength }]">{{
state.textNum
}}</view>
<view>/{{ maxLength }}</view>
</view>
</view>
</view>
</template>
<script lang="ts">
import { ref, toRefs, reactive, computed } from 'vue';
import { createComponent } from '@/utils/create';
const { componentName, create } = createComponent('textarea');
interface Events {
eventName: 'change' | 'focus' | 'blur' | 'clear' | 'update:value';
params: (string | number | Event)[];
}
export default create({
props: {
textAlign: {
type: String,
default: 'left'
},
limitShow: {
type: Boolean,
default: false
},
maxLength: {
type: String,
default: ''
},
rows: {
type: String,
default: ''
},
label: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '请输入信息'
},
readonly: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
autosize: {
type: Boolean,
default: false
},
value: {
type: [String, Number],
default: ''
}
},
emits: ['change', 'update:value', 'blur', 'focus', 'clear', 'error'],
setup(props, { emit }) {
const { maxLength } = props;
const { value } = toRefs(props);
const active = ref(false);
const state = reactive({
curretvalue: value,
textNum: String(value.value).length
});
const classes = computed(() => {
return {
[componentName]: true
};
});
const styles = computed(() => {
return {
'text-align': props.textAlign,
resize: props.autosize ? 'vertical' : 'none'
};
});
const emitChange = (envs: Array<Events>) => {
envs.forEach((item: Events) => {
return emit(item.eventName, ...item.params);
});
};
const valueChange = (e: Event) => {
const input = e.target as HTMLInputElement;
let val = input.value;
if (maxLength && val.length > Number(maxLength)) {
val = val.slice(0, Number(maxLength));
}
state.textNum = val.length;
emitChange([
{
eventName: 'update:value',
params: [val]
},
{
eventName: 'change',
params: [val]
}
]);
};
const valueFocus = (e: Event) => {
active.value = true;
const input = e.target as HTMLInputElement;
let val = input.value;
val = String(val);
emitChange([
{
eventName: 'update:value',
params: [state.curretvalue]
},
{
eventName: 'focus',
params: [val]
}
]);
};
const valueBlur = (e: Event) => {
setTimeout(() => {
active.value = false;
}, 400);
const input = e.target as HTMLInputElement;
let val = input.value;
val = String(val);
emitChange([
{
eventName: 'update:value',
params: [val]
},
{
eventName: 'blur',
params: [val]
}
]);
};
const handleClear = () => {
const val = '';
emitChange([
{
eventName: 'update:value',
params: [val]
},
{
eventName: 'clear',
params: [val]
}
]);
};
return {
props,
value,
state,
styles,
active,
maxLength,
valueChange,
valueFocus,
valueBlur,
handleClear,
emitChange
};
}
});
</script>
<style lang="scss">
@import 'index.scss';
</style>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册