diff --git a/components.json b/components.json index da3f772d4..dd4b25daa 100644 --- a/components.json +++ b/components.json @@ -6,6 +6,7 @@ "cell": "./packages/cell/index.js", "icon": "./packages/icon/index.js", "cell-group": "./packages/cell-group/index.js", + "cell-swipe": "./packages/cell-swipe/index.js", "popup": "./packages/popup/index.js", "dialog": "./packages/dialog/index.js", "picker": "./packages/picker/index.js", diff --git a/docs/examples-dist/cell-swipe.vue b/docs/examples-dist/cell-swipe.vue new file mode 100644 index 000000000..cd91bd534 --- /dev/null +++ b/docs/examples-dist/cell-swipe.vue @@ -0,0 +1,39 @@ +<template><section class="demo-cell"><h1 class="demo-title">Cell Swipe 滑动单元格</h1><example-block title="基础用法"> + <van-cell-swipe :right-width="65" :left-width="65"> + <van-cell-group> + <van-cell title="单元格1" value="单元格1内容"></van-cell> + </van-cell-group> + + <span slot="right" class="swipe-delete-btn"> + 删除 + </span> + <span slot="left" class="swipe-check-btn"> + 选择 + </span> +</van-cell-swipe> + + </example-block></section></template> +<style> +.swipe-delete-btn { + background-color: #FF4444; + color: #FFFFFF; + font-size: 16px; + width: 65px; + height: 44px; + display: inline-block; + text-align: center; + line-height: 44px; +} +.swipe-check-btn { + background-color: #84c483; + color: #FFFFFF; + font-size: 16px; + width: 65px; + height: 44px; + display: inline-block; + text-align: center; + line-height: 44px; +} +</style> +<script> +import Vue from "vue";import ExampleBlock from "components/example-block";Vue.component("example-block", ExampleBlock);</script> \ No newline at end of file diff --git a/docs/examples-dist/search.vue b/docs/examples-dist/search.vue index 24b981d97..0a05cfb6e 100644 --- a/docs/examples-dist/search.vue +++ b/docs/examples-dist/search.vue @@ -3,6 +3,9 @@ + </example-block><example-block title="基础用法"> + <van-search placeholder="搜索商品" type="showcase"></van-search> + </example-block><example-block title="监听对应事件"> <van-search placeholder="商品名称" @search="goSearch" @change="handleChange" @cancel="handleCancel"></van-search> diff --git a/docs/examples-docs/cell-swipe.md b/docs/examples-docs/cell-swipe.md new file mode 100644 index 000000000..51659b0be --- /dev/null +++ b/docs/examples-docs/cell-swipe.md @@ -0,0 +1,91 @@ + +<style> +.swipe-delete-btn { + background-color: #FF4444; + color: #FFFFFF; + font-size: 16px; + width: 65px; + height: 44px; + display: inline-block; + text-align: center; + line-height: 44px; +} +.swipe-check-btn { + background-color: #84c483; + color: #FFFFFF; + font-size: 16px; + width: 65px; + height: 44px; + display: inline-block; + text-align: center; + line-height: 44px; +} +</style> +## 滑动单元格 + +### 使用指南 + +如果你已经按照快速上手中引入了整个`vant`,以下**组件注册**就可以忽略了,因为你已经全局注册了`vant`中的全部组件。 + +#### 全局注册 + +你可以在全局注册`Cell Swipe`组件,比如页面的主文件(`index.js`,`main.js`),这样页面任何地方都可以直接使用`Cell Swipe`组件了: + +```js +import Vue from 'vue'; +import { CellSwipe } from 'vant'; +import 'vant/lib/vant-css/cell-swipe.css'; + +Vue.component(CellSwipe.name, CellSwipe); +``` + +#### 局部注册 + +如果你只是想在某个组件中使用,你可以在对应组件中注册`Cell Swipe`组件,这样只能在你注册的组件中使用`Cell Swipe`: + +```js +import { CellSwipe } from 'vant'; + +export default { + components: { + 'van-cell-swipe': CellSwipe + } +}; +``` + +### 代码演示 + +#### 基础用法 + +:::demo 基础用法 +```html +<van-cell-swipe :right-width="65" :left-width="65"> + <van-cell-group> + <van-cell title="单元格1" value="单元格1内容"></van-cell> + </van-cell-group> + + <span slot="right" class="swipe-delete-btn"> + 删除 + </span> + <span slot="left" class="swipe-check-btn"> + 选择 + </span> +</van-cell-swipe> +``` +::: + + +### API + +| 参数 | 说明 | 类型 | 默认值 | 可选值 | +|-----------|-----------|-----------|-------------|-------------| +| right-width | 右侧滑动按钮宽度 | `number` | 0 | | +| left-width | 左侧滑动按钮宽度 | `number` | 0 | | + +### Slot + +| name | 描述 | +|-----------|-----------| +| - | 自定义显示内容 | +| right | 右侧滑动内容 | +| left | 左侧滑动内容 | diff --git a/docs/src/nav.config.js b/docs/src/nav.config.js index b64adcc35..6f155b732 100644 --- a/docs/src/nav.config.js +++ b/docs/src/nav.config.js @@ -37,6 +37,10 @@ module.exports = { "path": "/cell", "title": "Cell 单元格" }, + { + "path": "/cell-swipe", + "title": "Cell Swipe 滑动单元格" + }, { "path": "/progress", "title": "Progress 进度条" diff --git a/packages/cell-swipe/components/CellSwipe.vue b/packages/cell-swipe/components/CellSwipe.vue new file mode 100644 index 000000000..777402261 --- /dev/null +++ b/packages/cell-swipe/components/CellSwipe.vue @@ -0,0 +1,142 @@ +<template> + <div + v-clickoutside:touchstart="swipeMove" + @click="swipeMove()" + @touchstart="startDrag" + @touchmove="onDrag" + @touchend="endDrag" + class="van-cell-swipe" + ref="cell"> + <div class="van-cell-wrapper"> + <slot>单元格内容</slot> + </div> + <div class="van-cell-left"> + <div ref="left"> + <slot name="left"></slot> + </div> + </div> + <div class="van-cell-right"> + <div ref="right"> + <slot name="right"></slot> + </div> + </div> + </div> +</template> + +<script> + import {once} from 'src/utils/dom'; + import Clickoutside from 'src/utils/clickoutside'; + + export default { + name: 'van-cell-swipe', + props: { + 'leftWidth': {type: Number, default: 0}, + 'rightWidth': {type: Number, default: 0} + }, + directives: {Clickoutside}, + data() { + return { + start: {x: 0, y: 0} + }; + }, + computed: { + leftDefaultTransform(){ + return this.translate3d(-this.leftWidth - 1); + }, + rightDefaultTransform(){ + return this.translate3d(this.rightWidth); + } + }, + mounted() { + this.wrap = this.$refs.cell.querySelector('.van-cell-wrapper'); + this.leftElm = this.$refs.left; + this.leftWrapElm = this.leftElm.parentNode; + this.leftDefaultTransform = this.translate3d(-this.leftWidth - 1); + this.leftWrapElm.style.webkitTransform = this.leftDefaultTransform; + + this.rightElm = this.$refs.right; + this.rightWrapElm = this.rightElm.parentNode; + this.rightDefaultTransform = this.translate3d(this.rightWidth); + this.rightWrapElm.style.webkitTransform = this.rightDefaultTransform; + }, + methods: { + resetSwipeStatus() { + this.swiping = false; // 是否正在拖动 + this.opened = true; // 记录是否滑动左右 或者 注册 + this.offsetLeft = 0; // 记录单次拖动的拖动距离 + }, + translate3d(offset) { + return `translate3d(${offset}px, 0, 0)`; + }, + swipeMove(offset = 0) { + this.wrap.style.webkitTransform = this.translate3d(offset); + this.rightWrapElm.style.webkitTransform = this.translate3d(this.rightWidth + offset); + this.leftWrapElm.style.webkitTransform = this.translate3d(-this.leftWidth + offset); + offset && (this.swiping = true); + }, + swipeLeaveTransition(direction) { + setTimeout(() => { + this.swipeLeave = true; + // left + if (direction > 0 && -this.offsetLeft > this.rightWidth * 0.4 && this.rightWidth > 0) { + this.swipeMove(-this.rightWidth); + this.resetSwipeStatus(); + return; + // right + } else if (direction < 0 && this.offsetLeft > this.leftWidth * 0.4 && this.leftWidth > 0) { + this.swipeMove(this.leftWidth); + this.resetSwipeStatus(); + return; + } else { + this.swipeMove(0); + once(this.wrap, 'webkitTransitionEnd', _ => { + this.wrap.style.webkitTransform = ''; + this.rightWrapElm.style.webkitTransform = this.rightDefaultTransform; + this.leftWrapElm.style.webkitTransform = this.leftDefaultTransform; + this.swipeLeave = false; + this.swiping = false; + }); + } + }, 0); + }, + startDrag(evt) { + console.log('startDrag') + evt = evt.changedTouches ? evt.changedTouches[0] : evt; + this.dragging = true; + this.start.x = evt.pageX; + this.start.y = evt.pageY; + }, + onDrag(evt) { + console.log('onDrag') + if (this.opened) { + !this.swiping && this.swipeMove(0); + this.opened = false; + return; + } + if (!this.dragging) return; + let swiping; + const e = evt.changedTouches ? evt.changedTouches[0] : evt; + const offsetTop = e.pageY - this.start.y; + const offsetLeft = this.offsetLeft = e.pageX - this.start.x; + if ((offsetLeft < 0 && -offsetLeft > this.rightWidth) || + (offsetLeft > 0 && offsetLeft > this.leftWidth) || + (offsetLeft > 0 && !this.leftWidth) || + (offsetLeft < 0 && !this.rightWidth)) { + return; + } + const y = Math.abs(offsetTop); + const x = Math.abs(offsetLeft); + swiping = !(x < 5 || (x >= 5 && y >= x * 1.73)); + if (!swiping) return; + evt.preventDefault(); + this.swipeMove(offsetLeft); + }, + endDrag() { + console.log('endDrag') + if (!this.swiping) return; + this.swipeLeaveTransition(this.offsetLeft > 0 ? -1 : 1); + } + } + }; +</script> + diff --git a/packages/cell-swipe/index.js b/packages/cell-swipe/index.js new file mode 100644 index 000000000..bb9559c21 --- /dev/null +++ b/packages/cell-swipe/index.js @@ -0,0 +1,2 @@ +import CellSwipe from './components/CellSwipe.vue' +export default CellSwipe; diff --git a/packages/vant-css/src/cell-swipe.css b/packages/vant-css/src/cell-swipe.css new file mode 100644 index 000000000..637e93854 --- /dev/null +++ b/packages/vant-css/src/cell-swipe.css @@ -0,0 +1,27 @@ + +.van-cell-swipe .van-cell-wrapper, .van-cell-swipe .van-cell-left, .van-cell-swipe .van-cell-right { + -webkit-transition: -webkit-transform 150ms ease-in-out; + transition: -webkit-transform 150ms ease-in-out; + transition: transform 150ms ease-in-out; + transition: transform 150ms ease-in-out, -webkit-transform 150ms ease-in-out; +} + +.van-cell-swipe{ + position: relative; + min-height: 48px; + overflow: hidden; +} +.van-cell-right{ + position: absolute; + height: 100%; + right: 0; + top: 0; + transform: translate3d(100%,0,0); +} +.van-cell-left { + position: absolute; + height: 100%; + left: 0; + top: 0; + transform: translate3d(-100%,0,0); +} \ No newline at end of file diff --git a/packages/vant-css/src/index.css b/packages/vant-css/src/index.css index 85785cd78..d83ee1789 100644 --- a/packages/vant-css/src/index.css +++ b/packages/vant-css/src/index.css @@ -4,6 +4,7 @@ @import './reset.css'; @import './button.css'; @import './cell.css'; +@import './cell-swipe.css'; @import './card.css'; @import './dialog.css'; @import './field.css'; diff --git a/src/index.js b/src/index.js index 9566cdfb8..37c200d8c 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ import Radio from '../packages/radio/index.js'; import Cell from '../packages/cell/index.js'; import Icon from '../packages/icon/index.js'; import CellGroup from '../packages/cell-group/index.js'; +import CellSwipe from '../packages/cell-swipe/index.js'; import Popup from '../packages/popup/index.js'; import Dialog from '../packages/dialog/index.js'; import Picker from '../packages/picker/index.js'; @@ -47,6 +48,7 @@ const install = function(Vue) { Vue.component(Cell.name, Cell); Vue.component(Icon.name, Icon); Vue.component(CellGroup.name, CellGroup); + Vue.component(CellSwipe.name, CellSwipe); Vue.component(Popup.name, Popup); Vue.component(Picker.name, Picker); Vue.component(RadioGroup.name, RadioGroup); @@ -89,6 +91,7 @@ module.exports = { Cell, Icon, CellGroup, + CellSwipe, Popup, Dialog, Picker, diff --git a/src/utils/dom.js b/src/utils/dom.js index ede0951ec..b5c2550a6 100644 --- a/src/utils/dom.js +++ b/src/utils/dom.js @@ -55,3 +55,44 @@ export function removeClass(el, cls) { el.className = trim(curClass); } }; +export const once = function(el, event, fn) { + var listener = function() { + if (fn) { + fn.apply(this, arguments); + } + off(el, event, listener); + }; + on(el, event, listener); +}; + +export const on = (function() { + if (document.addEventListener) { + return function(element, event, handler) { + if (element && event && handler) { + element.addEventListener(event, handler, false); + } + }; + } else { + return function(element, event, handler) { + if (element && event && handler) { + element.attachEvent('on' + event, handler); + } + }; + } +})(); + +export const off = (function() { + if (document.removeEventListener) { + return function(element, event, handler) { + if (element && event) { + element.removeEventListener(event, handler, false); + } + }; + } else { + return function(element, event, handler) { + if (element && event) { + element.detachEvent('on' + event, handler); + } + }; + } +})(); diff --git a/test/unit/specs/cell-swipe.spec.js b/test/unit/specs/cell-swipe.spec.js new file mode 100644 index 000000000..46325f291 --- /dev/null +++ b/test/unit/specs/cell-swipe.spec.js @@ -0,0 +1,141 @@ +import CellSwipe from 'packages/cell-swipe'; +import { mount } from 'avoriaz'; + +describe('CellSwipe', () => { + let wrapper; + afterEach(() => { + wrapper && wrapper.destroy(); + }); + + it('create a CellSwipe', () => { + wrapper = mount(CellSwipe, { + propsData: { + leftWidth: 2, + rightWidth: 2 + } + }); + wrapper.vm.startDrag({ + pageX: 0, + pageY: 0 + }); + wrapper.vm.onDrag({ + preventDefault() {}, + pageY: 0, + pageX: 50 + }); + wrapper.vm.offsetLeft = -20; + wrapper.vm.rightWidth = 10; + wrapper.vm.swipeLeaveTransition(1); + wrapper.vm.endDrag(); + expect(wrapper.hasClass('van-cell-swipe')).to.be.true; + }); +}); + + +describe('CellSwipe-left', () => { + let wrapper; + afterEach(() => { + wrapper && wrapper.destroy(); + }); + + it('create a CellSwipe left', () => { + wrapper = mount(CellSwipe, { + propsData: { + leftWidth: 2, + rightWidth: 2 + } + }); + wrapper.vm.startDrag({ + changedTouches: [{ + pageX: 0, + pageY: 0 + } + ] + }); + wrapper.vm.onDrag({ + preventDefault() {}, + changedTouches: [{ + pageX: 0, + pageY: -50 + } + ] + }); + wrapper.vm.offsetLeft = 20; + wrapper.vm.rightWidth = 10; + wrapper.vm.swipeLeaveTransition(-1); + wrapper.vm.endDrag(); + expect(wrapper.hasClass('van-cell-swipe')).to.be.true; + }); +}); + +describe('CellSwipe-0', () => { + let wrapper; + afterEach(() => { + wrapper && wrapper.destroy(); + }); + + it('create a CellSwipe 0', () => { + wrapper = mount(CellSwipe, { + propsData: { + leftWidth: 0, + rightWidth: 2 + } + }); + wrapper.vm.startDrag({ + pageX: 0, + pageY: 0 + }); + wrapper.vm.onDrag({ + preventDefault() {}, + pageY: 0, + pageX: -2 + }); + wrapper.vm.opened = true; + wrapper.vm.onDrag({ + preventDefault() {}, + pageY: 0, + pageX: -2 + }); + wrapper.vm.opened = false; + wrapper.vm.onDrag({ + preventDefault() {}, + pageY: 0, + pageX: 40 + }); + wrapper.vm.swipeLeaveTransition(0); + wrapper.vm.endDrag(); + expect(wrapper.hasClass('van-cell-swipe')).to.be.true; + }); +}); + + +describe('CellSwipe-0', () => { + let wrapper; + afterEach(() => { + wrapper && wrapper.destroy(); + }); + + it('create a CellSwipe 0', () => { + wrapper = mount(CellSwipe, { + propsData: { + leftWidth: 0, + rightWidth: 2 + } + }); + wrapper.vm.startDrag({ + pageX: 0, + pageY: 0 + }); + wrapper.vm.onDrag({ + preventDefault() {}, + pageY: 1000, + pageX: 40 + }); + wrapper.vm.swipeMove(); + wrapper.vm.swiping = false; + wrapper.vm.endDrag(); + wrapper.vm.swiping = true; + wrapper.vm.endDrag(); + expect(wrapper.hasClass('van-cell-swipe')).to.be.true; + }); +});