Commit 8e89ea2d82036f4fb1542170a46dadc46117f85c

Authored by 刘汉宸
1 parent 1b9e80db
Exists in master

feat: 新增输入框组件

.eslintignore
... ... @@ -2,4 +2,6 @@
2 2 public
3 3 node_modules
4 4 cache
5   -examples/assets
6 5 \ No newline at end of file
  6 +examples/assets
  7 +examples/views/docs
  8 +release
7 9 \ No newline at end of file
... ...
.prettierignore
... ... @@ -2,4 +2,6 @@
2 2 public
3 3 node_modules
4 4 cache
5   -examples/assets
6 5 \ No newline at end of file
  6 +examples/assets
  7 +examples/views/docs
  8 +release
7 9 \ No newline at end of file
... ...
examples/router/routes.js
... ... @@ -54,6 +54,12 @@ const _components = [
54 54 group: '表单组件',
55 55 children: [
56 56 {
  57 + path: 'input',
  58 + name: 'input',
  59 + meta: { title: 'Input 输入框' },
  60 + component: () => import('@/views/docs/component/input.md'),
  61 + },
  62 + {
57 63 path: 'checkbox',
58 64 name: 'checkbox',
59 65 meta: { title: 'Checkbox 复选框' },
... ...
examples/views/docs/component/input.md 0 → 100644
... ... @@ -0,0 +1,64 @@
  1 +# Input 输入框
  2 +
  3 +输入框组件,支持特殊业务场景格式化
  4 +
  5 +## 基本用法
  6 +
  7 +配置`type`属性设置类型
  8 +
  9 +::: snippet 支持`text`, `bankCard`, `password`, `phone`, `money`, `digit`,默认为`text`
  10 +
  11 +```html
  12 +<template>
  13 + <div>
  14 + <div class="input-field">
  15 + 提交值:{{ model }}
  16 + </div>
  17 + <div class="input-field">
  18 + 常规输入:<zui-input v-model="model" placeholder="常规输入" clearable />
  19 + </div>
  20 + <div class="input-field">
  21 + 银行卡输入:<zui-input v-model="model" placeholder="银行卡输入" type="bankCard" />
  22 + </div>
  23 + <div class="input-field">
  24 + 手机号输入:<zui-input v-model="model" placeholder="手机号输入" type="phone" />
  25 + </div>
  26 + <div class="input-field">
  27 + 金额输入:<zui-input v-model="model" placeholder="金额输入" type="money" />
  28 + </div>
  29 + </div>
  30 +</template>
  31 +
  32 +<script>
  33 +export default {
  34 + data() {
  35 + return {
  36 + model: ''
  37 + }
  38 + }
  39 +}
  40 +</script>
  41 +
  42 +<style>
  43 +.input-field {
  44 + padding: 10px;
  45 + border-bottom: 1px solid rgba(151, 151, 151, 0.1);
  46 +}
  47 +</style>
  48 +```
  49 +
  50 +:::
  51 +
  52 +
  53 +## API
  54 +
  55 +## Attribute 属性
  56 +
  57 +参数|说明|类型|可选值|默认值
  58 +-|-|-|-|-
  59 +value | 值 | String,Number | - | ''
  60 +placeholder | 值 | String,Number | - | ''
  61 +type | 类型 | String | text,bankCard,password,phone,money,digit | text
  62 +clearable | 是否可清除 | Boolean | - | false
  63 +disabled | 是否禁用 | Boolean | - | false
  64 +readonly | 是否只读 | Boolean | - | false
... ...
packages/icon/index.vue
1 1 <template>
2   - <i class="zui-icon" :class="classRender" :style="styleRender" @click.stop="onClick">
  2 + <i class="zui-icon" :class="classRender" :style="styleRender">
3 3 <div v-if="info || dot" class="zui-info" :class="{ dot: dot }">{{ info }}</div>
4 4 </i>
5 5 </template>
... ... @@ -38,13 +38,6 @@ export default {
38 38 };
39 39 },
40 40 },
41   - methods: {
42   - onClick: function () {
43   - if (this.$listeners['click']) {
44   - this.$emit('click');
45   - }
46   - },
47   - },
48 41 };
49 42 </script>
50 43  
... ...
packages/input/cursor.js 0 → 100644
... ... @@ -0,0 +1,50 @@
  1 +/**
  2 + * get position of input cursor
  3 + */
  4 +export function getCursorsPosition(ctrl) {
  5 + /* istanbul ignore if */
  6 + if (!ctrl) {
  7 + return 0;
  8 + }
  9 + let CaretPos = 0; // IE Support
  10 + /* istanbul ignore next */
  11 + if (document.selection) {
  12 + ctrl.focus();
  13 + const Sel = document.selection.createRange();
  14 + Sel.moveStart('character', -ctrl.value.length);
  15 + CaretPos = Sel.text.length;
  16 + } else if (ctrl.selectionStart || ctrl.selectionStart === '0') {
  17 + // Firefox support
  18 + CaretPos = ctrl.selectionStart;
  19 + }
  20 + return CaretPos;
  21 +}
  22 +
  23 +let timer = null;
  24 +/**
  25 + * set position of input cursor
  26 + */
  27 +export function setCursorsPosition(ctrl, pos) {
  28 + /* istanbul ignore if */
  29 + if (!ctrl) {
  30 + return;
  31 + }
  32 + /* istanbul ignore if */
  33 + if (timer) {
  34 + clearTimeout(timer);
  35 + }
  36 +
  37 + timer = setTimeout(() => {
  38 + /* istanbul ignore next */
  39 + if (ctrl.setSelectionRange) {
  40 + ctrl.focus();
  41 + ctrl.setSelectionRange(pos, pos);
  42 + } else if (ctrl.createTextRange) {
  43 + const range = ctrl.createTextRange();
  44 + range.collapse(true);
  45 + range.moveEnd('character', pos);
  46 + range.moveStart('character', pos);
  47 + range.select();
  48 + }
  49 + }, 50);
  50 +}
... ...
packages/input/formate-value.js 0 → 100644
... ... @@ -0,0 +1,76 @@
  1 +export function formatValueByGapRule(gapRule, value, gap = ' ', range, isAdd = 1) {
  2 + const arr = value ? value.split('') : [];
  3 + let showValue = '';
  4 + const rule = [];
  5 + gapRule.split('|').some((n, j) => {
  6 + rule[j] = +n + (rule[j - 1] ? +rule[j - 1] : 0);
  7 + });
  8 + let j = 0;
  9 + arr.some((n, i) => {
  10 + // Remove the excess part
  11 + if (i > rule[rule.length - 1] - 1) {
  12 + return;
  13 + }
  14 + if (i > 0 && i === rule[j]) {
  15 + showValue = showValue + gap + n;
  16 + j++;
  17 + } else {
  18 + showValue = showValue + '' + n;
  19 + }
  20 + });
  21 + let adapt = 0;
  22 + rule.some((n, j) => {
  23 + if (range === +n + 1 + j) {
  24 + adapt = 1 * isAdd;
  25 + }
  26 + });
  27 + range = typeof range !== 'undefined' ? (range === 0 ? 0 : range + adapt) : showValue.length;
  28 + return { value: showValue, range: range };
  29 +}
  30 +
  31 +export function formatValueByGapStep(step, value, gap = ' ', direction = 'right', range, isAdd = 1, oldValue = '') {
  32 + if (value.length === 0) {
  33 + return { value, range };
  34 + }
  35 +
  36 + const arr = value && value.split('');
  37 + let _range = range;
  38 + let showValue = '';
  39 +
  40 + if (direction === 'right') {
  41 + for (let j = arr.length - 1, k = 0; j >= 0; j--, k++) {
  42 + const m = arr[j];
  43 + showValue = k > 0 && k % step === 0 ? m + gap + showValue : m + '' + showValue;
  44 + }
  45 + if (isAdd === 1) {
  46 + // 在添加的情况下,如果添加前字符串的长度减去新的字符串的长度为2,说明多了一个间隔符,需要调整range
  47 + if (oldValue.length - showValue.length === -2) {
  48 + _range = range + 1;
  49 + }
  50 + } else {
  51 + // 在删除情况下,如果删除前字符串的长度减去新的字符串的长度为2,说明少了一个间隔符,需要调整range
  52 + if (oldValue.length - showValue.length === 2) {
  53 + _range = range - 1;
  54 + }
  55 + // 删除到最开始,range 保持 0
  56 + if (_range <= 0) {
  57 + _range = 0;
  58 + }
  59 + }
  60 + } else {
  61 + arr.some((n, i) => {
  62 + showValue = i > 0 && i % step === 0 ? showValue + gap + n : showValue + '' + n;
  63 + });
  64 + const adapt = range % (step + 1) === 0 ? 1 * isAdd : 0;
  65 + _range = typeof range !== 'undefined' ? (range === 0 ? 0 : range + adapt) : showValue.length;
  66 + }
  67 +
  68 + return { value: showValue, range: _range };
  69 +}
  70 +
  71 +export function trimValue(value, gap = ' ') {
  72 + value = typeof value === 'undefined' ? '' : value;
  73 + const reg = new RegExp(gap, 'g');
  74 + value = value.toString().replace(reg, '');
  75 + return value;
  76 +}
... ...
packages/input/index.vue 0 → 100644
... ... @@ -0,0 +1,278 @@
  1 +<template>
  2 + <div class="zui-input">
  3 + <input
  4 + class="zui-input__input"
  5 + :type="inputType"
  6 + :name="name"
  7 + v-model="inputBindValue"
  8 + :placeholder="inputPlaceholder"
  9 + :disabled="disabled"
  10 + :readonly="readonly"
  11 + :maxlength="isInputFormative ? '' : maxlength"
  12 + autocomplete="off"
  13 + @focus="$_onFocus"
  14 + @blur="$_onBlur"
  15 + @keyup="$_onKeyup"
  16 + @keydown="$_onKeydown"
  17 + @input="$_onInput"
  18 + />
  19 + <div class="zui-input__clear" v-if="clearable && !disabled && !readonly" v-show="!isInputEmpty" @click="$_clearInput">
  20 + <slot v-if="$slots.clear" name="clear"></slot>
  21 + <zui-icon v-else name="close"></zui-icon>
  22 + </div>
  23 + <slot name="right"></slot>
  24 + </div>
  25 +</template>
  26 +
  27 +<script>
  28 +import ZuiIcon from '../icon';
  29 +import { getCursorsPosition, setCursorsPosition } from './cursor';
  30 +import { formatValueByGapRule, formatValueByGapStep, trimValue } from './formate-value';
  31 +
  32 +export default {
  33 + name: 'Input',
  34 + components: {
  35 + ZuiIcon,
  36 + },
  37 + props: {
  38 + value: {
  39 + type: [String, Number],
  40 + default: '',
  41 + },
  42 + placeholder: {
  43 + type: String,
  44 + default: '',
  45 + },
  46 + maxlength: {
  47 + type: [String, Number],
  48 + default: '',
  49 + },
  50 + type: {
  51 + // text, bankCard, password, phone, money, digit
  52 + type: String,
  53 + default: 'text',
  54 + },
  55 + name: {
  56 + type: [String, Number],
  57 + },
  58 + clearable: {
  59 + type: Boolean,
  60 + default: false,
  61 + },
  62 + disabled: {
  63 + type: Boolean,
  64 + default: false,
  65 + },
  66 + readonly: {
  67 + type: Boolean,
  68 + default: false,
  69 + },
  70 + isFormative: {
  71 + type: Boolean,
  72 + default: false,
  73 + },
  74 + isTitleLatent: {
  75 + type: Boolean,
  76 + default: false,
  77 + },
  78 + formation: {
  79 + type: Function,
  80 + default: function () {},
  81 + },
  82 + },
  83 + data() {
  84 + return {
  85 + inputValue: '',
  86 + inputBindValue: '',
  87 + isInputFocus: false,
  88 + };
  89 + },
  90 + computed: {
  91 + inputItemType() {
  92 + return this.type || 'text';
  93 + },
  94 + inputType() {
  95 + let inputType = this.inputItemType || 'text';
  96 + if (inputType === 'bankCard' || inputType === 'phone' || inputType === 'digit') {
  97 + inputType = 'tel';
  98 + } else if (inputType === 'money') {
  99 + inputType = 'text';
  100 + }
  101 + return inputType;
  102 + },
  103 + isInputEmpty() {
  104 + return !this.inputValue.length;
  105 + },
  106 + isInputFormative() {
  107 + const type = this.inputItemType;
  108 + return this.isFormative || type === 'bankCard' || type === 'phone' || type === 'money' || type === 'digit';
  109 + },
  110 + inputPlaceholder() {
  111 + return this.isTitleLatent && this.isInputActive ? '' : this.placeholder;
  112 + },
  113 + isInputActive() {
  114 + return !this.isInputEmpty || this.isInputFocus;
  115 + },
  116 + },
  117 + watch: {
  118 + value(val) {
  119 + // Filter out two-way binding
  120 + if (val !== this.$_trimValue(this.inputValue)) {
  121 + this.inputValue = this.$_formateValue(this.$_subValue(val + '')).value;
  122 + }
  123 + },
  124 + inputValue(val) {
  125 + this.inputBindValue = val;
  126 + val = this.isInputFormative ? this.$_trimValue(val) : val;
  127 + if (val !== this.value) {
  128 + this.$emit('input', val);
  129 + this.$emit('change', this.name, val);
  130 + }
  131 + },
  132 + },
  133 + created() {
  134 + this.inputValue = this.$_formateValue(this.$_subValue(this.value + '')).value;
  135 + },
  136 + methods: {
  137 + // MARK: private methods
  138 + $_formateValue(curValue, curPos = 0) {
  139 + const type = this.inputItemType;
  140 + const name = this.name;
  141 + const oldValue = this.inputValue;
  142 + const isAdd = oldValue.length > curValue.length ? -1 : 1;
  143 +
  144 + let formateValue = { value: curValue, range: curPos };
  145 +
  146 + // no format
  147 + if (!this.isInputFormative || curValue === '') {
  148 + return formateValue;
  149 + }
  150 +
  151 + // custom format by user
  152 + const customValue = this.formation(name, curValue, curPos);
  153 +
  154 + if (customValue) {
  155 + return customValue;
  156 + }
  157 +
  158 + // default format by component
  159 + let gap = ' ';
  160 + switch (type) {
  161 + case 'bankCard':
  162 + curValue = this.$_subValue(trimValue(curValue.replace(/\D/g, '')));
  163 + formateValue = formatValueByGapStep(4, curValue, gap, 'left', curPos, isAdd, oldValue);
  164 + break;
  165 + case 'phone':
  166 + curValue = this.$_subValue(trimValue(curValue.replace(/\D/g, '')));
  167 + formateValue = formatValueByGapRule('3|4|4', curValue, gap, curPos, isAdd);
  168 + break;
  169 + case 'money':
  170 + gap = ',';
  171 + curValue = this.$_subValue(trimValue(curValue.replace(/[^\d.]/g, '')));
  172 + // curValue = curValue.replace(/\D/g, '')
  173 + // eslint-disable-next-line no-case-declarations
  174 + const dotPos = curValue.indexOf('.');
  175 + // format if no dot or new add dot or insert befor dot
  176 + // eslint-disable-next-line no-case-declarations
  177 + const moneyCurValue = curValue.split('.')[0];
  178 + // eslint-disable-next-line no-case-declarations
  179 + const moneyCurDecimal = ~dotPos ? `.${curValue.split('.')[1]}` : '';
  180 +
  181 + formateValue = formatValueByGapStep(3, trimValue(moneyCurValue, gap), gap, 'right', curPos, isAdd, oldValue.split('.')[0]);
  182 + formateValue.value += moneyCurDecimal;
  183 + break;
  184 + case 'digit':
  185 + curValue = this.$_subValue(trimValue(curValue.replace(/[^\d.]/g, '')));
  186 + formateValue.value = curValue;
  187 + break;
  188 + /* istanbul ignore next */
  189 + default:
  190 + break;
  191 + }
  192 +
  193 + return formateValue;
  194 + },
  195 + $_subValue(val) {
  196 + const len = this.inputMaxLength;
  197 + if (len !== '') {
  198 + return val.substring(0, len);
  199 + } else {
  200 + return val;
  201 + }
  202 + },
  203 + $_onFocus() {
  204 + this.isInputFocus = true;
  205 + this.$emit('focus', this.name);
  206 + },
  207 + $_onBlur() {
  208 + setTimeout(() => {
  209 + this.isInputFocus = false;
  210 + this.$emit('blur', this.name);
  211 + }, 100);
  212 + },
  213 + $_onKeyup(event) {
  214 + this.$emit('keyup', this.name, event);
  215 + if (+event.keyCode === 13 || +event.keyCode === 108) {
  216 + this.$emit('confirm', this.name, this.inputValue);
  217 + }
  218 + },
  219 + $_onKeydown(event) {
  220 + this.$emit('keydown', this.name, event);
  221 + },
  222 + $_onInput(event) {
  223 + const formateValue = this.$_formateValue(event.target.value, this.isInputFormative ? getCursorsPosition(event.target) : 0);
  224 +
  225 + this.inputValue = formateValue.value;
  226 + this.inputBindValue = formateValue.value;
  227 +
  228 + if (this.isInputFormative) {
  229 + this.$nextTick(() => {
  230 + setCursorsPosition(event.target, formateValue.range);
  231 + });
  232 + }
  233 + },
  234 + $_trimValue(val) {
  235 + return trimValue(val, '\\s|,');
  236 + },
  237 + $_clearInput() {
  238 + this.inputValue = '';
  239 + },
  240 + // MARK: public methods
  241 + focus() {
  242 + this.$el.querySelector('.zui-input__input').focus();
  243 + setTimeout(() => {
  244 + this.isInputFocus = true;
  245 + }, 200);
  246 + },
  247 + blur() {
  248 + this.$el.querySelector('.zui-input__input').blur();
  249 + this.isInputFocus = false;
  250 + },
  251 + getValue() {
  252 + return this.inputValue;
  253 + },
  254 + },
  255 +};
  256 +</script>
  257 +
  258 +<style lang="scss">
  259 +.zui-input {
  260 + min-height: 1.5rem;
  261 + box-sizing: border-box;
  262 + display: flex;
  263 + align-items: center;
  264 + justify-content: space-between;
  265 + &__input {
  266 + border: none;
  267 + outline: none;
  268 + flex: auto;
  269 + width: 100%;
  270 + }
  271 + &__clear {
  272 + white-space: nowrap;
  273 + word-break: break-all;
  274 + display: inline-block;
  275 + color: $color-minor;
  276 + }
  277 +}
  278 +</style>
... ...