From c92791e8dab9bb7f53a1580c005de985f35a6af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=98=89=E6=B6=B5?= Date: Wed, 8 May 2019 15:39:27 +0800 Subject: [PATCH] [new feature] add IndexBar component --- docs/markdown/v2-progress-tracking.md | 3 +- docs/src/WapApp.vue | 5 + docs/src/demo-entry.js | 1 + docs/src/doc.config.js | 8 ++ docs/src/docs-entry.js | 2 + packages/index-anchor/index.js | 26 ++++++ packages/index-anchor/index.less | 8 ++ packages/index-bar/demo/index.vue | 64 +++++++++++++ packages/index-bar/en-US.md | 75 +++++++++++++++ packages/index-bar/index.js | 92 +++++++++++++++++++ packages/index-bar/index.less | 22 +++++ .../test/__snapshots__/demo.spec.js.snap | 21 +++++ .../test/__snapshots__/index.spec.js.snap | 9 ++ packages/index-bar/test/demo.spec.js | 4 + packages/index-bar/test/index.spec.js | 85 +++++++++++++++++ packages/index-bar/zh-CN.md | 79 ++++++++++++++++ packages/index.less | 2 + packages/index.ts | 6 ++ packages/mixins/relation.js | 6 +- 19 files changed, 515 insertions(+), 3 deletions(-) create mode 100644 packages/index-anchor/index.js create mode 100644 packages/index-anchor/index.less create mode 100644 packages/index-bar/demo/index.vue create mode 100644 packages/index-bar/en-US.md create mode 100644 packages/index-bar/index.js create mode 100644 packages/index-bar/index.less create mode 100644 packages/index-bar/test/__snapshots__/demo.spec.js.snap create mode 100644 packages/index-bar/test/__snapshots__/index.spec.js.snap create mode 100644 packages/index-bar/test/demo.spec.js create mode 100644 packages/index-bar/test/index.spec.js create mode 100644 packages/index-bar/zh-CN.md diff --git a/docs/markdown/v2-progress-tracking.md b/docs/markdown/v2-progress-tracking.md index 181a168f1..a6873f327 100644 --- a/docs/markdown/v2-progress-tracking.md +++ b/docs/markdown/v2-progress-tracking.md @@ -52,7 +52,8 @@ ## 新特性 - 新增`Skeleton`骨架屏组件 -- 新增`DropdownMenu`下拉菜单组件 +- 新增`IndexBar`、`IndexAnchor`索引栏组件 +- 新增`DropdownMenu`、`DropdownItem`下拉菜单组件 ### ActionSheet diff --git a/docs/src/WapApp.vue b/docs/src/WapApp.vue index 19a08fa30..a0ff5a598 100644 --- a/docs/src/WapApp.vue +++ b/docs/src/WapApp.vue @@ -75,6 +75,11 @@ body { -webkit-font-smoothing: antialiased; } +::-webkit-scrollbar { + width: 0; + background: transparent; +} + .van-doc-nav-bar { height: 56px; line-height: 56px; diff --git a/docs/src/demo-entry.js b/docs/src/demo-entry.js index 167bc55f8..c0a78f54a 100644 --- a/docs/src/demo-entry.js +++ b/docs/src/demo-entry.js @@ -23,6 +23,7 @@ export default { 'goods-action': () => wrapper(import('../../packages/goods-action/demo'), 'goods-action'), 'icon': () => wrapper(import('../../packages/icon/demo'), 'icon'), 'image-preview': () => wrapper(import('../../packages/image-preview/demo'), 'image-preview'), + 'index-bar': () => wrapper(import('../../packages/index-bar/demo'), 'index-bar'), 'lazyload': () => wrapper(import('../../packages/lazyload/demo'), 'lazyload'), 'list': () => wrapper(import('../../packages/list/demo'), 'list'), 'loading': () => wrapper(import('../../packages/loading/demo'), 'loading'), diff --git a/docs/src/doc.config.js b/docs/src/doc.config.js index f416f57f8..2afcae2e3 100644 --- a/docs/src/doc.config.js +++ b/docs/src/doc.config.js @@ -259,6 +259,10 @@ module.exports = { groupName: '导航组件', icon: 'https://img.yzcdn.cn/vant/nav-0401.svg', list: [ + { + path: '/index-bar', + title: 'IndexBar 索引栏' + }, { path: '/nav-bar', title: 'NavBar 导航栏' @@ -574,6 +578,10 @@ module.exports = { groupName: 'Navigation Components', icon: 'https://img.yzcdn.cn/vant/nav-0401.svg', list: [ + { + path: '/index-bar', + title: 'IndexBar' + }, { path: '/nav-bar', title: 'NavBar' diff --git a/docs/src/docs-entry.js b/docs/src/docs-entry.js index d3a8d2fff..0651306f9 100644 --- a/docs/src/docs-entry.js +++ b/docs/src/docs-entry.js @@ -53,6 +53,8 @@ export default { 'icon.zh-CN': () => import('../../packages/icon/zh-CN.md'), 'image-preview.en-US': () => import('../../packages/image-preview/en-US.md'), 'image-preview.zh-CN': () => import('../../packages/image-preview/zh-CN.md'), + 'index-bar.en-US': () => import('../../packages/index-bar/en-US.md'), + 'index-bar.zh-CN': () => import('../../packages/index-bar/zh-CN.md'), 'lazyload.en-US': () => import('../../packages/lazyload/en-US.md'), 'lazyload.zh-CN': () => import('../../packages/lazyload/zh-CN.md'), 'list.en-US': () => import('../../packages/list/en-US.md'), diff --git a/packages/index-anchor/index.js b/packages/index-anchor/index.js new file mode 100644 index 000000000..327bacd3d --- /dev/null +++ b/packages/index-anchor/index.js @@ -0,0 +1,26 @@ +import { use } from '../utils'; +import { ChildrenMixin } from '../mixins/relation'; + +const [sfc, bem] = use('index-anchor'); + +export default sfc({ + mixins: [ChildrenMixin('vanIndexBar', { indexKey: 'childrenIndex' })], + + props: { + index: [String, Number] + }, + + methods: { + scrollIntoView() { + this.$el.scrollIntoView(); + } + }, + + render(h) { + return ( +
+ {this.slots('default') ? this.slots('default') : this.index} +
+ ); + } +}); diff --git a/packages/index-anchor/index.less b/packages/index-anchor/index.less new file mode 100644 index 000000000..62f6a03f0 --- /dev/null +++ b/packages/index-anchor/index.less @@ -0,0 +1,8 @@ +@import '../style/var'; + +.van-index-anchor { + padding: 0 15px; + font-size: 14px; + font-weight: 500; + line-height: 32px; +} diff --git a/packages/index-bar/demo/index.vue b/packages/index-bar/demo/index.vue new file mode 100644 index 000000000..5862b6024 --- /dev/null +++ b/packages/index-bar/demo/index.vue @@ -0,0 +1,64 @@ + + + diff --git a/packages/index-bar/en-US.md b/packages/index-bar/en-US.md new file mode 100644 index 000000000..35838e692 --- /dev/null +++ b/packages/index-bar/en-US.md @@ -0,0 +1,75 @@ +## IndexBar + +### Install + +``` javascript +import { IndexBar } from 'vant'; + +Vue.use(IndexBar); +``` + +### Usage + +#### Basic Usage + +```html + + + + + + + + + + + + ... + +``` + +#### Custom Index List + +```html + + Title 1 + + + + + Title 2 + + + + + ... + +``` + +```js +export default { + data() { + return { + indexList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + } + } +} +``` + +### IndexBar Props + +| Attribute | Description | Type | Default | +|------|------|------|------| +| index-list | Index List | `Array` | `A-Z` | + +### IndexAnchor Props + +| Attribute | Description | Type | Default | +|------|------|------|------| +| index | Index | `String | Number` | - | + +### IndexAnchor Slots + +| Name | Description | +|------|------| +| default | Anchor content, show index by default | diff --git a/packages/index-bar/index.js b/packages/index-bar/index.js new file mode 100644 index 000000000..c2fff1722 --- /dev/null +++ b/packages/index-bar/index.js @@ -0,0 +1,92 @@ +import { use } from '../utils'; +import { TouchMixin } from '../mixins/touch'; +import { ParentMixin } from '../mixins/relation'; + +const [sfc, bem] = use('index-bar'); + +export default sfc({ + mixins: [TouchMixin, ParentMixin('vanIndexBar')], + + props: { + indexList: { + type: Array, + default() { + const indexList = []; + const charCodeOfA = 'A'.charCodeAt(0); + + for (let i = 0; i < 26; i++) { + indexList.push(String.fromCharCode(charCodeOfA + i)); + } + + return indexList; + } + } + }, + + methods: { + onClick(event) { + this.scrollToElement(event.target); + }, + + onTouchStart(event) { + this.touchStart(event); + }, + + onTouchMove(event) { + this.touchMove(event); + + if (this.direction === 'vertical') { + /* istanbul ignore else */ + if (event.cancelable) { + event.preventDefault(); + } + + const { clientX, clientY } = event.touches[0]; + const target = document.elementFromPoint(clientX, clientY); + this.scrollToElement(target); + } + }, + + scrollToElement(element, setActive) { + if (!element) { + return; + } + + const { index } = element.dataset; + if (!index) { + return; + } + + const match = this.children.filter(item => String(item.index) === index); + if (match[0]) { + match[0].scrollIntoView(); + } + }, + + onTouchEnd() { + this.active = null; + } + }, + + render(h) { + return ( +
+
+ {this.indexList.map(index => ( + + {index} + + ))} +
+ {this.slots('default')} +
+ ); + } +}); diff --git a/packages/index-bar/index.less b/packages/index-bar/index.less new file mode 100644 index 000000000..8d606d1bb --- /dev/null +++ b/packages/index-bar/index.less @@ -0,0 +1,22 @@ +@import '../style/var'; + +.van-index-bar { + &__sidebar { + position: fixed; + display: flex; + top: 50%; + right: 0; + z-index: 1; + user-select: none; + text-align: center; + flex-direction: column; + transform: translateY(-50%); + } + + &__index { + font-size: 10px; + font-weight: 500; + line-height: 14px; + padding: 0 3px 0 15px; + } +} diff --git a/packages/index-bar/test/__snapshots__/demo.spec.js.snap b/packages/index-bar/test/__snapshots__/demo.spec.js.snap new file mode 100644 index 000000000..5f6173977 --- /dev/null +++ b/packages/index-bar/test/__snapshots__/demo.spec.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders demo correctly 1`] = ` +
+
+
+
+
+
+
+
+ + +
+
+
+`; diff --git a/packages/index-bar/test/__snapshots__/index.spec.js.snap b/packages/index-bar/test/__snapshots__/index.spec.js.snap new file mode 100644 index 000000000..9873e0449 --- /dev/null +++ b/packages/index-bar/test/__snapshots__/index.spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`custom anchor text 1`] = ` +
+
ABCDEFGHIJKLMNOPQRSTUVWXYZ
+
Title A
+
Title B
+
+`; diff --git a/packages/index-bar/test/demo.spec.js b/packages/index-bar/test/demo.spec.js new file mode 100644 index 000000000..d647cfabc --- /dev/null +++ b/packages/index-bar/test/demo.spec.js @@ -0,0 +1,4 @@ +import Demo from '../demo'; +import demoTest from '../../../test/demo-test'; + +demoTest(Demo); diff --git a/packages/index-bar/test/index.spec.js b/packages/index-bar/test/index.spec.js new file mode 100644 index 000000000..7319fa19b --- /dev/null +++ b/packages/index-bar/test/index.spec.js @@ -0,0 +1,85 @@ +import { mount, trigger, triggerDrag } from '../../../test/utils'; +import Vue from 'vue'; +import IndexBar from '..'; +import IndexAnchor from '../../index-anchor'; + +Vue.use(IndexBar); +Vue.use(IndexAnchor); + +function mockScrollIntoView() { + const fn = jest.fn(); + Element.prototype.scrollIntoView = fn; + return fn; +} + +test('custom anchor text', () => { + const wrapper = mount({ + template: ` + + Title A + Title B + + ` + }); + + expect(wrapper).toMatchSnapshot(); +}); + +test('click and scroll to anchor', () => { + const wrapper = mount({ + template: ` + + + + + ` + }); + + const fn = mockScrollIntoView(); + const indexes = wrapper.findAll('.van-index-bar__index'); + indexes.at(0).trigger('click'); + expect(fn).toHaveBeenCalledTimes(1); +}); + +test('touch and scroll to anchor', () => { + const wrapper = mount({ + template: ` + + + + + + ` + }); + + const fn = mockScrollIntoView(); + const sidebar = wrapper.find('.van-index-bar__sidebar'); + const indexes = wrapper.findAll('.van-index-bar__index'); + + document.elementFromPoint = function (x, y) { + const index = y / 100; + + if (index === 1 || index === 2) { + return indexes.at(index).element; + } + + if (index === 3) { + return { + dataset: {} + }; + } + }; + + // horizontal drag + triggerDrag(sidebar, 100, 0); + expect(fn).toHaveBeenCalledTimes(0); + + // vertiacl drag + trigger(sidebar, 'touchstart', 0, 0); + trigger(sidebar, 'touchmove', 0, 100); + trigger(sidebar, 'touchmove', 0, 200); + trigger(sidebar, 'touchmove', 0, 300); + trigger(sidebar, 'touchmove', 0, 400); + trigger(sidebar, 'touchend', 0, 400); + expect(fn).toHaveBeenCalledTimes(1); +}); diff --git a/packages/index-bar/zh-CN.md b/packages/index-bar/zh-CN.md new file mode 100644 index 000000000..75df19b0b --- /dev/null +++ b/packages/index-bar/zh-CN.md @@ -0,0 +1,79 @@ +## IndexBar 索引栏 + +### 使用指南 + +``` javascript +import { IndexBar, IndexAnchor } from 'vant'; + +Vue.use(IndexBar).use(IndexAnchor); +``` + +### 代码演示 + +#### 基础用法 + +点击索引栏时,会自动跳转到对应的`IndexAnchor`锚点位置 + +```html + + + + + + + + + + + + ... + +``` + +#### 自定义索引列表 + +可以通过`index-list`属性自定义展示的索引字符列表, + +```html + + 标题1 + + + + + 标题2 + + + + + ... + +``` + +```js +export default { + data() { + return { + indexList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + } + } +} +``` + +### IndexBar Props + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +|------|------|------|------|------| +| index-list | 索引字符列表 | `Array` | `A-Z` | - | + +### IndexAnchor Props + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +|------|------|------|------|------| +| index | 索引字符 | `String | Number` | - | - | + +### IndexAnchor Slots + +| 名称 | 说明 | +|------|------| +| default | 锚点位置显示内容,默认为索引字符 | diff --git a/packages/index.less b/packages/index.less index b069e1e0c..c05cab5ea 100644 --- a/packages/index.less +++ b/packages/index.less @@ -34,6 +34,8 @@ @import './stepper/index'; @import './swipe/index'; @import './swipe-item/index'; +@import './index-anchor/index'; +@import './index-bar/index'; /* form components */ @import './checkbox/index'; diff --git a/packages/index.ts b/packages/index.ts index 3694875e7..82ce439d4 100644 --- a/packages/index.ts +++ b/packages/index.ts @@ -31,6 +31,8 @@ import GoodsActionButton from './goods-action-button'; import GoodsActionIcon from './goods-action-icon'; import Icon from './icon'; import ImagePreview from './image-preview'; +import IndexAnchor from './index-anchor'; +import IndexBar from './index-bar'; import Info from './info'; import Lazyload from './lazyload'; import List from './list'; @@ -114,6 +116,8 @@ const components = [ GoodsActionIcon, Icon, ImagePreview, + IndexAnchor, + IndexBar, Info, List, Loading, @@ -202,6 +206,8 @@ export { GoodsActionIcon, Icon, ImagePreview, + IndexAnchor, + IndexBar, Info, Lazyload, List, diff --git a/packages/mixins/relation.js b/packages/mixins/relation.js index 1f100daec..1ca3b3c86 100644 --- a/packages/mixins/relation.js +++ b/packages/mixins/relation.js @@ -1,4 +1,6 @@ -export function ChildrenMixin(parent) { +export function ChildrenMixin(parent, options = {}) { + const indexKey = options.indexKey || 'index'; + return { inject: { [parent]: { @@ -11,7 +13,7 @@ export function ChildrenMixin(parent) { return this[parent]; }, - index() { + [indexKey]() { this.bindRelation(); return this.parent.children.indexOf(this); }