mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-06 03:57:59 +08:00
add NumberKeyboard component
This commit is contained in:
parent
c221922e04
commit
f0cbcc99dc
101
docs/examples-docs/number-keyboard.md
Normal file
101
docs/examples-docs/number-keyboard.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<style>
|
||||||
|
.demo-number-keyboard {
|
||||||
|
.van-button {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Toast } from 'packages';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showKeyboard: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onInput(value) {
|
||||||
|
Toast('Input: ' + value);
|
||||||
|
},
|
||||||
|
onDelete() {
|
||||||
|
Toast('Delete');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## NumberKeyboard 数字键盘
|
||||||
|
|
||||||
|
### 使用指南
|
||||||
|
``` javascript
|
||||||
|
import { NumberKeyboard } from 'vant';
|
||||||
|
|
||||||
|
Vue.component(NumberKeyboard.name, NumberKeyboard);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码演示
|
||||||
|
|
||||||
|
#### 基础用法
|
||||||
|
|
||||||
|
:::demo 基础用法
|
||||||
|
```html
|
||||||
|
<van-button @touchstart.native.stop="showKeyboard = true">
|
||||||
|
弹出键盘
|
||||||
|
</van-button>
|
||||||
|
|
||||||
|
<van-button @touchstart.native.stop="showKeyboard = false">
|
||||||
|
收起键盘
|
||||||
|
</van-button>
|
||||||
|
|
||||||
|
<van-number-keyboard
|
||||||
|
:show="showKeyboard"
|
||||||
|
@blur="showKeyboard = false"
|
||||||
|
@input="onInput"
|
||||||
|
@delete="onDelete"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showKeyboard: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onInput(value) {
|
||||||
|
Toast(value);
|
||||||
|
},
|
||||||
|
onDelete() {
|
||||||
|
Toast('delete');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
| 参数 | 说明 | 类型 | 默认值 | 可选值 |
|
||||||
|
|-----------|-----------|-----------|-------------|-------------|
|
||||||
|
| show | 是否显示键盘 | `Boolean` | - | - |
|
||||||
|
| title | 键盘标题 | `String` | `安全输入键盘` | - |
|
||||||
|
| extraKey | 左下角按键内容 | `String` | `''` | - |
|
||||||
|
| zIndex | 键盘 z-index | `Number` | `100` | - |
|
||||||
|
| transition | 是否开启过场动画 | `Boolean` | `true` | - |
|
||||||
|
| showDeleteKey | 是否展示删除按钮 | `Boolean` | `true` | - |
|
||||||
|
|
||||||
|
### Event
|
||||||
|
|
||||||
|
| 事件名 | 说明 | 参数 |
|
||||||
|
|-----------|-----------|-----------|
|
||||||
|
| input | 点击按键时触发 | key: 按键内容 |
|
||||||
|
| delete | 点击删除键时触发 | - |
|
||||||
|
| blur | 点击非键盘区域时触发 | - |
|
||||||
|
| show | 键盘完全弹出时触发 | - |
|
||||||
|
| hide | 键盘完全收起时触发 | - |
|
@ -143,6 +143,10 @@ module.exports = {
|
|||||||
"path": "/field",
|
"path": "/field",
|
||||||
"title": "Field 输入框"
|
"title": "Field 输入框"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "/number-keyboard",
|
||||||
|
"title": "NumberKeyboard 数字键盘"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "/radio",
|
"path": "/radio",
|
||||||
"title": "Radio 单选框"
|
"title": "Radio 单选框"
|
||||||
|
@ -24,6 +24,7 @@ import Lazyload from './lazyload';
|
|||||||
import Loading from './loading';
|
import Loading from './loading';
|
||||||
import NavBar from './nav-bar';
|
import NavBar from './nav-bar';
|
||||||
import NoticeBar from './notice-bar';
|
import NoticeBar from './notice-bar';
|
||||||
|
import NumberKeyboard from './number-keyboard';
|
||||||
import Panel from './panel';
|
import Panel from './panel';
|
||||||
import Picker from './picker';
|
import Picker from './picker';
|
||||||
import Popup from './popup';
|
import Popup from './popup';
|
||||||
@ -74,6 +75,7 @@ const components = [
|
|||||||
Loading,
|
Loading,
|
||||||
NavBar,
|
NavBar,
|
||||||
NoticeBar,
|
NoticeBar,
|
||||||
|
NumberKeyboard,
|
||||||
Panel,
|
Panel,
|
||||||
Picker,
|
Picker,
|
||||||
Popup,
|
Popup,
|
||||||
@ -140,6 +142,7 @@ export {
|
|||||||
Loading,
|
Loading,
|
||||||
NavBar,
|
NavBar,
|
||||||
NoticeBar,
|
NoticeBar,
|
||||||
|
NumberKeyboard,
|
||||||
Panel,
|
Panel,
|
||||||
Picker,
|
Picker,
|
||||||
Popup,
|
Popup,
|
||||||
|
137
packages/number-keyboard/index.vue
Normal file
137
packages/number-keyboard/index.vue
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<transition :name="transition ? 'van-slide-bottom' : ''">
|
||||||
|
<div
|
||||||
|
v-show="show"
|
||||||
|
:style="style"
|
||||||
|
class="van-number-keyboard"
|
||||||
|
@touchstart.stop.prevent="focus"
|
||||||
|
@touchmove="blurKey"
|
||||||
|
@touchend="blurKey"
|
||||||
|
@touchcancel="blurKey"
|
||||||
|
@animationend="onAnimationEnd"
|
||||||
|
>
|
||||||
|
<h3 class="van-number-keyboard__title van-hairline--top">
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
v-for="(key, index) in keys"
|
||||||
|
v-text="key"
|
||||||
|
:data-key="index"
|
||||||
|
:class="['van-hairline', {
|
||||||
|
'van-number-keyboard--active': index === active,
|
||||||
|
'van-number-keyboard__delete': index === 11 && showDeleteKey
|
||||||
|
}]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'van-number-keyboard',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
show: Boolean,
|
||||||
|
extraKey: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '安全输入键盘'
|
||||||
|
},
|
||||||
|
zIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 100
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showDeleteKey: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.handler(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
this.handler(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
activated() {
|
||||||
|
this.handler(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
deactivated() {
|
||||||
|
this.handler(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
active: -1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
show() {
|
||||||
|
if (!this.transition) {
|
||||||
|
this.$emit(this.show ? 'show' : 'hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
keys() {
|
||||||
|
const keys = [];
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const key = i === 10 ? 0 : i < 9 ? i + 1 : i === 9 ? this.extraKey : '';
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
},
|
||||||
|
|
||||||
|
style() {
|
||||||
|
return {
|
||||||
|
zIndex: this.zIndex
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handler(action) {
|
||||||
|
if (action !== this.handlerStatus) {
|
||||||
|
this.handlerStatus = action;
|
||||||
|
document.body[(action ? 'add' : 'remove') + 'EventListener']('touchstart', this.blurKeyboard);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
focus(event) {
|
||||||
|
this.active = parseInt(event.target.dataset.key);
|
||||||
|
if (this.active === 11) {
|
||||||
|
this.$emit('delete');
|
||||||
|
} else if (!isNaN(this.active)) {
|
||||||
|
const key = this.keys[this.active];
|
||||||
|
if (key !== '') {
|
||||||
|
this.$emit('input', key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
blurKey() {
|
||||||
|
this.active = -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
blurKeyboard() {
|
||||||
|
this.$emit('blur');
|
||||||
|
},
|
||||||
|
|
||||||
|
onAnimationEnd() {
|
||||||
|
this.$emit(this.show ? 'show' : 'hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -5,4 +5,5 @@
|
|||||||
@import "./common/var.css";
|
@import "./common/var.css";
|
||||||
@import "./common/normalize.css";
|
@import "./common/normalize.css";
|
||||||
@import "./common/hairline.css";
|
@import "./common/hairline.css";
|
||||||
|
@import "./common/animation.css";
|
||||||
|
|
||||||
|
21
packages/vant-css/src/common/animation.css
Normal file
21
packages/vant-css/src/common/animation.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@keyframes van-slide-bottom-enter {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 100%, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes van-slide-bottom-leave {
|
||||||
|
to {
|
||||||
|
transform: translate3d(0, 100%, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-slide-bottom {
|
||||||
|
&-enter-active {
|
||||||
|
animation: van-slide-bottom-enter .3s both ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-leave-active {
|
||||||
|
animation: van-slide-bottom-leave .3s both ease;
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,7 @@
|
|||||||
@import './radio.css';
|
@import './radio.css';
|
||||||
@import './switch.css';
|
@import './switch.css';
|
||||||
@import './uploader.css';
|
@import './uploader.css';
|
||||||
|
@import './number-keyboard.css';
|
||||||
|
|
||||||
/* action components */
|
/* action components */
|
||||||
@import './actionsheet.css';
|
@import './actionsheet.css';
|
||||||
|
52
packages/vant-css/src/number-keyboard.css
Normal file
52
packages/vant-css/src/number-keyboard.css
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
@import "./common/var.css";
|
||||||
|
|
||||||
|
.van-number-keyboard {
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
user-select: none;
|
||||||
|
background-color: $white;
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
color: $gray-dark;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
width: calc(100%/3);
|
||||||
|
height: 54px;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 54px;
|
||||||
|
font-style: normal;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:nth-of-type(3n))::after {
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-of-type(10),
|
||||||
|
&:nth-of-type(12) {
|
||||||
|
background-color: #F3F3F6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__delete {
|
||||||
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAeCAMAAABg6AyVAAAAbFBMVEUAAAAfHiIdHB4eHR8dHR4eHB4dHB4dHR8gICIdHB4dHB4dHB4dHB8eHh8hISEeHR8fHB8fHR8fHR8fHx8eHiArKyszMzMeHB8eHB8fHR8eHiAeHh4dHB4vLjDY2Nn////b29zKysq9vb28vLzkfBRpAAAAHHRSTlMAK/PW+I/llBv77N1kSCPwWlFAOTMGBb28hHlu08g5sgAAAMlJREFUOMuV1MsWgiAQgGHQyOx+s+sgYO//jnnMGIdDDfwbN99CYEDQFiVEKkolPUG7gl9VTWC31NKuDbVz+Fc1tRJtPDmxS2BS3p5ZC+XXnnbAVoz2WEBCH7uZAalzGoa06whGiznT6sG2xgX4QO2Aej1+KN7XBKL2FvGaMtTWBhbQhtoaYzVQrHKwuGf8hhAPSF5g3xPSt45sCHcouNWx436FGA+RHyQcD35EcUj54U8ff4WYvVi1zLjelUh/OG6XjOeLWv5hfAOI+HLwwOAqhAAAAABJRU5ErkJggg==") no-repeat center center;
|
||||||
|
background-size: auto 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i&--active {
|
||||||
|
background-color: $active-color!important;
|
||||||
|
}
|
||||||
|
}
|
146
test/unit/specs/number-keyboard.spec.js
Normal file
146
test/unit/specs/number-keyboard.spec.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import NumberKeyboard from 'packages/number-keyboard';
|
||||||
|
import { mount } from 'avoriaz';
|
||||||
|
import { triggerTouch } from '../utils';
|
||||||
|
|
||||||
|
function mockKeyDown(wrapper, keyIndex) {
|
||||||
|
const customEvent = document.createEvent('CustomEvent');
|
||||||
|
customEvent.initCustomEvent('touchstart', true, true, {});
|
||||||
|
Object.defineProperty(customEvent, 'target', {
|
||||||
|
value: {
|
||||||
|
dataset: {
|
||||||
|
key: keyIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wrapper.element.dispatchEvent(customEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('NumberKeyboard', () => {
|
||||||
|
let wrapper;
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper && wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a NumberKeyboard', () => {
|
||||||
|
wrapper = mount(NumberKeyboard, {});
|
||||||
|
expect(wrapper.hasClass('van-number-keyboard')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('click a keyboard key', (done) => {
|
||||||
|
wrapper = mount(NumberKeyboard, {});
|
||||||
|
|
||||||
|
// just for coverage
|
||||||
|
wrapper.vm.handler(true);
|
||||||
|
|
||||||
|
wrapper.vm.$on('input', value => {
|
||||||
|
expect(value).to.equal(1);
|
||||||
|
expect(wrapper.vm.active).to.equal(0);
|
||||||
|
|
||||||
|
triggerTouch(wrapper, 'touchend');
|
||||||
|
expect(wrapper.vm.active).to.equal(-1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
mockKeyDown(wrapper, 9);
|
||||||
|
mockKeyDown(wrapper, NaN);
|
||||||
|
mockKeyDown(wrapper, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('click delete key', (done) => {
|
||||||
|
wrapper = mount(NumberKeyboard, {});
|
||||||
|
|
||||||
|
const deleteSpy = sinon.spy();
|
||||||
|
wrapper.vm.$on('delete', deleteSpy);
|
||||||
|
|
||||||
|
mockKeyDown(wrapper, 11);
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
expect(deleteSpy.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blur keyboard', (done) => {
|
||||||
|
wrapper = mount(NumberKeyboard, {
|
||||||
|
attachToDocument: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const blur = sinon.spy();
|
||||||
|
wrapper.vm.$on('blur', blur);
|
||||||
|
|
||||||
|
triggerTouch(document.body, 'touchstart');
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
expect(blur.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listen to show event when has transtion', (done) => {
|
||||||
|
wrapper = mount(NumberKeyboard, {
|
||||||
|
attachToDocument: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const show = sinon.spy();
|
||||||
|
wrapper.vm.$on('show', show);
|
||||||
|
wrapper.vm.show = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(show.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
}, 800);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listen to show event when no transtion', (done) => {
|
||||||
|
wrapper = mount(NumberKeyboard, {
|
||||||
|
attachToDocument: true,
|
||||||
|
propsData: {
|
||||||
|
transition: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const show = sinon.spy();
|
||||||
|
wrapper.vm.$on('show', show);
|
||||||
|
wrapper.vm.show = true;
|
||||||
|
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
expect(show.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listen to hide event when has transtion', (done) => {
|
||||||
|
wrapper = mount(NumberKeyboard, {
|
||||||
|
attachToDocument: true,
|
||||||
|
propsData: {
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hide = sinon.spy();
|
||||||
|
wrapper.vm.$on('hide', hide);
|
||||||
|
wrapper.vm.show = false;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(hide.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
}, 800);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listen to hide event when no transtion', (done) => {
|
||||||
|
wrapper = mount(NumberKeyboard, {
|
||||||
|
attachToDocument: true,
|
||||||
|
propsData: {
|
||||||
|
show: true,
|
||||||
|
transition: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hide = sinon.spy();
|
||||||
|
wrapper.vm.$on('hide', hide);
|
||||||
|
wrapper.vm.show = false;
|
||||||
|
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
expect(hide.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user