From 216e4eb8e6c84d67ccdfb203ac2f242d32f143d0 Mon Sep 17 00:00:00 2001 From: neverland Date: Wed, 5 Sep 2018 11:06:59 +0800 Subject: [PATCH] [new feature] add Tab component (#496) --- dist/field/index.js | 4 +- dist/tab/index.js | 31 ++++++ dist/tab/index.json | 3 + dist/tab/index.wxml | 7 ++ dist/tabs/index.js | 178 ++++++++++++++++++++++++++++++++ dist/tabs/index.json | 3 + dist/tabs/index.wxml | 26 +++++ dist/tabs/index.wxss | 1 + example/app.json | 3 +- example/config.js | 4 + example/pages/tab/index.js | 29 ++++++ example/pages/tab/index.json | 8 ++ example/pages/tab/index.wxml | 70 +++++++++++++ example/pages/tab/index.wxss | 4 + example/pages/tabbar/index.json | 2 +- packages/tab/README.md | 144 ++++++++++++++++++++++++++ packages/tab/index.js | 31 ++++++ packages/tab/index.json | 3 + packages/tab/index.wxml | 7 ++ packages/tabs/index.js | 178 ++++++++++++++++++++++++++++++++ packages/tabs/index.json | 3 + packages/tabs/index.pcss | 125 ++++++++++++++++++++++ packages/tabs/index.wxml | 26 +++++ 23 files changed, 886 insertions(+), 4 deletions(-) create mode 100644 dist/tab/index.js create mode 100644 dist/tab/index.json create mode 100644 dist/tab/index.wxml create mode 100644 dist/tabs/index.js create mode 100644 dist/tabs/index.json create mode 100644 dist/tabs/index.wxml create mode 100644 dist/tabs/index.wxss create mode 100644 example/pages/tab/index.js create mode 100644 example/pages/tab/index.json create mode 100644 example/pages/tab/index.wxml create mode 100644 example/pages/tab/index.wxss create mode 100644 packages/tab/README.md create mode 100644 packages/tab/index.js create mode 100644 packages/tab/index.json create mode 100644 packages/tab/index.wxml create mode 100644 packages/tabs/index.js create mode 100644 packages/tabs/index.json create mode 100644 packages/tabs/index.pcss create mode 100644 packages/tabs/index.wxml diff --git a/dist/field/index.js b/dist/field/index.js index ca6be95e..268c7303 100644 --- a/dist/field/index.js +++ b/dist/field/index.js @@ -55,8 +55,8 @@ Component({ value: true }, titleWidth: { - type: Number, - value: 90 + type: String, + value: '90px' } }, diff --git a/dist/tab/index.js b/dist/tab/index.js new file mode 100644 index 00000000..0f5cdaea --- /dev/null +++ b/dist/tab/index.js @@ -0,0 +1,31 @@ +const TABS_PATH = '../tabs/index'; + +Component({ + options: { + addGlobalClass: true + }, + + properties: { + disabled: Boolean, + title: { + type: String, + observer() { + const parent = this.getRelationNodes(TABS_PATH)[0]; + if (parent) { + parent.setLine(); + } + } + } + }, + + relations: { + [TABS_PATH]: { + type: 'ancestor' + } + }, + + data: { + inited: false, + active: false + } +}); diff --git a/dist/tab/index.json b/dist/tab/index.json new file mode 100644 index 00000000..32640e0d --- /dev/null +++ b/dist/tab/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/dist/tab/index.wxml b/dist/tab/index.wxml new file mode 100644 index 00000000..ee7168f3 --- /dev/null +++ b/dist/tab/index.wxml @@ -0,0 +1,7 @@ + + + diff --git a/dist/tabs/index.js b/dist/tabs/index.js new file mode 100644 index 00000000..bfa428b8 --- /dev/null +++ b/dist/tabs/index.js @@ -0,0 +1,178 @@ +const TAB_PATH = '../tab/index'; + +Component({ + options: { + addGlobalClass: true + }, + + relations: { + [TAB_PATH]: { + type: 'descendant', + + linked(target) { + const { tabs } = this.data; + tabs.push({ + instance: target, + data: target.data + }); + this.setData({ + tabs, + scrollable: tabs.length > this.data.swipeThreshold + }); + this.setActiveTab(); + }, + + unlinked(target) { + const tabs = this.data.tabs.filter(item => item.instance !== target); + this.setData({ + tabs, + scrollable: tabs.length > this.data.swipeThreshold + }); + this.setActiveTab(); + } + } + }, + + properties: { + color: { + type: String, + observer: 'setLine' + }, + lineWidth: { + type: Number, + observer: 'setLine' + }, + active: { + type: null, + value: 0 + }, + type: { + type: String, + value: 'line' + }, + duration: { + type: Number, + value: 0.2 + }, + swipeThreshold: { + type: Number, + value: 4, + observer() { + this.setData({ + scrollable: this.data.tabs.length > this.data.swipeThreshold + }); + } + } + }, + + data: { + tabs: [], + lineStyle: '', + scrollLeft: 0 + }, + + ready() { + this.setLine(); + this.scrollIntoView(); + }, + + methods: { + trigger(eventName, index) { + this.triggerEvent(eventName, { + index, + title: this.data.tabs[index].data.title + }); + }, + + onTap(event) { + const { index } = event.currentTarget.dataset; + if (this.data.tabs[index].data.disabled) { + this.trigger('disabled', index); + } else { + this.trigger('click', index); + this.setActive(index); + } + }, + + setActive(active) { + if (active !== this.data.active) { + this.trigger('change', active); + this.setData({ active }); + this.setActiveTab(); + this.setLine(); + this.scrollIntoView(); + } + }, + + getRect(selector, callback, all) { + wx.createSelectorQuery() + .in(this)[all ? 'selectAll' : 'select'](selector) + .boundingClientRect(rect => { + rect && callback(rect); + }) + .exec(); + }, + + setLine() { + if (this.data.type !== 'line') { + return; + } + + this.getRect('.van-tab', rects => { + const rect = rects[this.data.active]; + const width = this.data.lineWidth || rect.width; + let left = rects + .slice(0, this.data.active) + .reduce((prev, curr) => prev + curr.width, 0); + left += (rect.width - width) / 2; + + this.setData({ + lineStyle: ` + width: ${width}px; + background-color: ${this.data.color}; + transform: translateX(${left}px); + transition-duration: ${this.data.duration}s; + ` + }); + }, true); + }, + + setActiveTab() { + this.data.tabs.forEach((item, index) => { + const data = { + active: index === this.data.active + }; + + if (data.active) { + data.inited = true; + } + + if (data.active !== item.instance.data.active) { + item.instance.setData(data); + } + }); + }, + + // scroll active tab into view + scrollIntoView(immediate) { + if (!this.data.scrollable) { + return; + } + + this.getRect('.van-tab', tabRects => { + const tabRect = tabRects[this.data.active]; + const offsetLeft = tabRects + .slice(0, this.data.active) + .reduce((prev, curr) => prev + curr.width, 0); + const tabWidth = tabRect.width; + + this.getRect('.van-tabs__nav', navRect => { + const navWidth = navRect.width; + this.setData({ + scrollLeft: offsetLeft - (navWidth - tabWidth) / 2 + }); + }); + }, true); + } + } +}); diff --git a/dist/tabs/index.json b/dist/tabs/index.json new file mode 100644 index 00000000..32640e0d --- /dev/null +++ b/dist/tabs/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/dist/tabs/index.wxml b/dist/tabs/index.wxml new file mode 100644 index 00000000..906d1977 --- /dev/null +++ b/dist/tabs/index.wxml @@ -0,0 +1,26 @@ + + + + + + + {{ item.data.title }} + + + + + + + + diff --git a/dist/tabs/index.wxss b/dist/tabs/index.wxss new file mode 100644 index 00000000..10ca63cb --- /dev/null +++ b/dist/tabs/index.wxss @@ -0,0 +1 @@ +.van-tabs{position:relative;-webkit-tap-highlight-color:transparent}.van-tabs__wrap{top:0;left:0;right:0;z-index:99;position:absolute}.van-tabs__wrap--page-top{position:fixed}.van-tabs__wrap--content-bottom{top:auto;bottom:0}.van-tabs__wrap--scrollable .van-tab{-webkit-box-flex:0;-webkit-flex:0 0 22%;flex:0 0 22%}.van-tabs__nav{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-user-select:none;user-select:none;position:relative;background-color:#fff}.van-tabs__nav--line{height:100%}.van-tabs__nav--card{margin:0 15px;border-radius:2px;box-sizing:border-box;border:1px solid #f44;height:30px}.van-tabs__nav--card .van-tab{color:#f44;border-right:1px solid #f44;line-height:28px}.van-tabs__nav--card .van-tab:last-child{border-right:none}.van-tabs__nav--card .van-tab.van-tab--active{color:#fff;background-color:#f44}.van-tabs__line{z-index:1;left:0;bottom:0;height:2px;position:absolute;background-color:#f44}.van-tabs--line{padding-top:44px}.van-tabs--line .van-tabs__wrap{height:44px}.van-tabs--card{padding-top:30px}.van-tabs--card .van-tabs__wrap{height:30px}.van-tab{-webkit-box-flex:1;-webkit-flex:1;flex:1;cursor:pointer;padding:0 5px;font-size:14px;position:relative;color:#333;line-height:44px;text-align:center;box-sizing:border-box;background-color:#fff;min-width:0}.van-tab span{display:block}.van-tab:active{background-color:#e8e8e8}.van-tab--active{color:#f44}.van-tab--disabled{color:#c9c9c9}.van-tab--disabled:active{background-color:#fff} \ No newline at end of file diff --git a/example/app.json b/example/app.json index 1a349204..72abe890 100644 --- a/example/app.json +++ b/example/app.json @@ -22,9 +22,10 @@ "pages/switch-cell/index", "pages/search/index", "pages/slider/index", + "pages/tab/index", + "pages/tabbar/index", "pages/tag/index", "pages/toast/index", - "pages/tabbar/index", "pages/transition/index", "pages/tree-select/index" ], diff --git a/example/config.js b/example/config.js index 11f34fa4..66ce11f4 100644 --- a/example/config.js +++ b/example/config.js @@ -54,6 +54,10 @@ export default [ path: '/tag', title: 'Tag 标记' }, + { + path: '/tab', + title: 'Tab 标签页' + }, { path: '/tabbar', title: 'Tabbar 标签栏' diff --git a/example/pages/tab/index.js b/example/pages/tab/index.js new file mode 100644 index 00000000..9906a9b9 --- /dev/null +++ b/example/pages/tab/index.js @@ -0,0 +1,29 @@ +import Page from '../../common/page'; + +Page({ + data: { + tabs: [1, 2, 3, 4], + tabsMore: [1, 2, 3, 4, 5, 6, 7, 8] + }, + + onClickDisabled(event) { + wx.showToast({ + title: `标签 ${event.detail.index + 1} 已被禁用`, + icon: 'none' + }); + }, + + onChange(event) { + wx.showToast({ + title: `切换到标签 ${event.detail.index + 1}`, + icon: 'none' + }); + }, + + onClick(event) { + wx.showToast({ + title: `点击标签 ${event.detail.index + 1}`, + icon: 'none' + }); + } +}); diff --git a/example/pages/tab/index.json b/example/pages/tab/index.json new file mode 100644 index 00000000..987bfe1a --- /dev/null +++ b/example/pages/tab/index.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "Tab 标签页", + "usingComponents": { + "demo-block": "../../components/demo-block/index", + "van-tab": "../../dist/tab/index", + "van-tabs": "../../dist/tabs/index" + } +} diff --git a/example/pages/tab/index.wxml b/example/pages/tab/index.wxml new file mode 100644 index 00000000..0a553e7c --- /dev/null +++ b/example/pages/tab/index.wxml @@ -0,0 +1,70 @@ + + + + + {{ '内容' + item }} + + + + + + + + + + {{ '内容' + item }} + + + + + + + + + + {{ '内容' + item }} + + + + + + + + + + {{ '内容' + item }} + + + + + + + + + + {{ '内容' + item }} + + + + diff --git a/example/pages/tab/index.wxss b/example/pages/tab/index.wxss new file mode 100644 index 00000000..559600a6 --- /dev/null +++ b/example/pages/tab/index.wxss @@ -0,0 +1,4 @@ +.content { + padding: 20px; + background-color: #fff; +} diff --git a/example/pages/tabbar/index.json b/example/pages/tabbar/index.json index 2bde3863..e2d61606 100644 --- a/example/pages/tabbar/index.json +++ b/example/pages/tabbar/index.json @@ -1,5 +1,5 @@ { - "navigationBarTitleText": "Tabbar 标签页", + "navigationBarTitleText": "Tabbar 标签栏", "usingComponents": { "demo-block": "../../components/demo-block/index", "van-tabbar": "../../dist/tabbar/index", diff --git a/packages/tab/README.md b/packages/tab/README.md new file mode 100644 index 00000000..fb386ff0 --- /dev/null +++ b/packages/tab/README.md @@ -0,0 +1,144 @@ +## Tab 标签页 + +### 使用指南 +在 index.json 中引入组件 +```json +"usingComponents": { + "van-tab": "path/to/vant-weapp/dist/tab/index", + "van-tabs": "path/to/vant-weapp/dist/tabs/index" +} +``` + +### 代码演示 + +#### 基础用法 + +默认情况下启用第一个标签,可以通过`active`设定当前激活的标签索引,在回调参数的`event.detail`中可以取得被点击标签的标题和索引 + +```html + + 内容 1 + 内容 2 + 内容 3 + 内容 4 + +``` + +```js +Page({ + data: { + active: 1 + }, + + onChange(event) { + wx.showToast({ + title: `切换到标签 ${event.detail.index + 1}`, + icon: 'none' + }); + } +}); +``` + +#### 横向滚动 + +多于 4 个标签时,Tab 可以横向滚动 + +```html + + 内容 1 + 内容 2 + 内容 3 + 内容 4 + 内容 5 + 内容 6 + +``` + +#### 禁用标签 + +设置`disabled`属性即可禁用标签。如果需要监听禁用标签的点击事件,可以在`van-tabs`上监听`disabled`事件 + +```html + + 内容 1 + 内容 2 + 内容 3 + +``` + +```javascript +Page({ + onClickDisabled(event) { + wx.showToast({ + title: `标签 ${event.detail.index + 1} 已被禁用`, + icon: 'none' + }); + } +}); +``` + +#### 样式风格 + +`Tab`支持两种样式风格:`line`和`card`,默认为`line`样式,可以通过`type`属性修改样式风格 + +```html + + 内容 1 + 内容 2 + 内容 3 + +``` + +#### 点击事件 + +可以在`van-tabs`上绑定`click`事件,在回调参数的`event.detail`中可以取得被点击标签的标题和索引 + +```html + + 内容 1 + 内容 2 + +``` + +```javascript +Page({ + onClick(event) { + wx.showToast({ + title: `点击标签 ${event.detail.index + 1}`, + icon: 'none' + }); + } +}); +``` + +### Tabs API + +| 参数 | 说明 | 类型 | 默认值 | +|-----------|-----------|-----------|-------------| +| active | 当前激活标签的索引 | `String` `Number` | `0` | +| color | 标签颜色 | `String` | `#f44` | +| type | 样式风格,可选值为 `card` | `String` | `line` | +| duration | 动画时间 (单位秒)) | `Number` | `0.2` | +| line-width | 底部条宽度 (px) | `Number` | 与当前标签等宽 | +| swipe-threshold | 滚动阈值,设置标签数量超过多少个可滚动 | `Number` | `4` | + +### Tab API + +| 参数 | 说明 | 类型 | 默认值 | +|-----------|-----------|-----------|-------------| +| title | 标题 | `String` | - | +| disabled | 是否禁用标签 | `Boolean` | `false` | + +### Tab Slot + +| 名称 | 说明 | +|-----------|-----------| +| - | 标签页内容 | + +### Tabs Event + +| 事件名 | 说明 | 参数 | +|-----------|-----------|-----------| +| bind:click | 点击标签时触发 | index:标签索引,title:标题 | +| bind:change | 当前激活的标签改变时触发 | index:标签索引,title:标题 | +| bind:disabled | 点击被禁用的标签时触发 | index:标签索引,title:标题 | diff --git a/packages/tab/index.js b/packages/tab/index.js new file mode 100644 index 00000000..0f5cdaea --- /dev/null +++ b/packages/tab/index.js @@ -0,0 +1,31 @@ +const TABS_PATH = '../tabs/index'; + +Component({ + options: { + addGlobalClass: true + }, + + properties: { + disabled: Boolean, + title: { + type: String, + observer() { + const parent = this.getRelationNodes(TABS_PATH)[0]; + if (parent) { + parent.setLine(); + } + } + } + }, + + relations: { + [TABS_PATH]: { + type: 'ancestor' + } + }, + + data: { + inited: false, + active: false + } +}); diff --git a/packages/tab/index.json b/packages/tab/index.json new file mode 100644 index 00000000..32640e0d --- /dev/null +++ b/packages/tab/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/packages/tab/index.wxml b/packages/tab/index.wxml new file mode 100644 index 00000000..ee7168f3 --- /dev/null +++ b/packages/tab/index.wxml @@ -0,0 +1,7 @@ + + + diff --git a/packages/tabs/index.js b/packages/tabs/index.js new file mode 100644 index 00000000..bfa428b8 --- /dev/null +++ b/packages/tabs/index.js @@ -0,0 +1,178 @@ +const TAB_PATH = '../tab/index'; + +Component({ + options: { + addGlobalClass: true + }, + + relations: { + [TAB_PATH]: { + type: 'descendant', + + linked(target) { + const { tabs } = this.data; + tabs.push({ + instance: target, + data: target.data + }); + this.setData({ + tabs, + scrollable: tabs.length > this.data.swipeThreshold + }); + this.setActiveTab(); + }, + + unlinked(target) { + const tabs = this.data.tabs.filter(item => item.instance !== target); + this.setData({ + tabs, + scrollable: tabs.length > this.data.swipeThreshold + }); + this.setActiveTab(); + } + } + }, + + properties: { + color: { + type: String, + observer: 'setLine' + }, + lineWidth: { + type: Number, + observer: 'setLine' + }, + active: { + type: null, + value: 0 + }, + type: { + type: String, + value: 'line' + }, + duration: { + type: Number, + value: 0.2 + }, + swipeThreshold: { + type: Number, + value: 4, + observer() { + this.setData({ + scrollable: this.data.tabs.length > this.data.swipeThreshold + }); + } + } + }, + + data: { + tabs: [], + lineStyle: '', + scrollLeft: 0 + }, + + ready() { + this.setLine(); + this.scrollIntoView(); + }, + + methods: { + trigger(eventName, index) { + this.triggerEvent(eventName, { + index, + title: this.data.tabs[index].data.title + }); + }, + + onTap(event) { + const { index } = event.currentTarget.dataset; + if (this.data.tabs[index].data.disabled) { + this.trigger('disabled', index); + } else { + this.trigger('click', index); + this.setActive(index); + } + }, + + setActive(active) { + if (active !== this.data.active) { + this.trigger('change', active); + this.setData({ active }); + this.setActiveTab(); + this.setLine(); + this.scrollIntoView(); + } + }, + + getRect(selector, callback, all) { + wx.createSelectorQuery() + .in(this)[all ? 'selectAll' : 'select'](selector) + .boundingClientRect(rect => { + rect && callback(rect); + }) + .exec(); + }, + + setLine() { + if (this.data.type !== 'line') { + return; + } + + this.getRect('.van-tab', rects => { + const rect = rects[this.data.active]; + const width = this.data.lineWidth || rect.width; + let left = rects + .slice(0, this.data.active) + .reduce((prev, curr) => prev + curr.width, 0); + left += (rect.width - width) / 2; + + this.setData({ + lineStyle: ` + width: ${width}px; + background-color: ${this.data.color}; + transform: translateX(${left}px); + transition-duration: ${this.data.duration}s; + ` + }); + }, true); + }, + + setActiveTab() { + this.data.tabs.forEach((item, index) => { + const data = { + active: index === this.data.active + }; + + if (data.active) { + data.inited = true; + } + + if (data.active !== item.instance.data.active) { + item.instance.setData(data); + } + }); + }, + + // scroll active tab into view + scrollIntoView(immediate) { + if (!this.data.scrollable) { + return; + } + + this.getRect('.van-tab', tabRects => { + const tabRect = tabRects[this.data.active]; + const offsetLeft = tabRects + .slice(0, this.data.active) + .reduce((prev, curr) => prev + curr.width, 0); + const tabWidth = tabRect.width; + + this.getRect('.van-tabs__nav', navRect => { + const navWidth = navRect.width; + this.setData({ + scrollLeft: offsetLeft - (navWidth - tabWidth) / 2 + }); + }); + }, true); + } + } +}); diff --git a/packages/tabs/index.json b/packages/tabs/index.json new file mode 100644 index 00000000..32640e0d --- /dev/null +++ b/packages/tabs/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/packages/tabs/index.pcss b/packages/tabs/index.pcss new file mode 100644 index 00000000..210b9e47 --- /dev/null +++ b/packages/tabs/index.pcss @@ -0,0 +1,125 @@ +@import '../common/style/var.pcss'; + +$van-tabs-line-height: 44px; +$van-tabs-card-height: 30px; + +.van-tabs { + position: relative; + -webkit-tap-highlight-color: transparent; + + &__wrap { + top: 0; + left: 0; + right: 0; + z-index: 99; + position: absolute; + + &--page-top { + position: fixed; + } + + &--content-bottom { + top: auto; + bottom: 0; + } + + &--scrollable { + .van-tab { + flex: 0 0 22%; + } + } + } + + &__nav { + display: flex; + user-select: none; + position: relative; + background-color: $white; + + &--line { + height: 100%; + } + + &--card { + margin: 0 15px; + border-radius: 2px; + box-sizing: border-box; + border: 1px solid $red; + height: $van-tabs-card-height; + + .van-tab { + color: $red; + border-right: 1px solid $red; + line-height: calc($van-tabs-card-height - 2px); + + &:last-child { + border-right: none; + } + + &.van-tab--active { + color: $white; + background-color: $red; + } + } + } + } + + &__line { + z-index: 1; + left: 0; + bottom: 0; + height: 2px; + position: absolute; + background-color: $red; + } + + &--line { + padding-top: $van-tabs-line-height; + + .van-tabs__wrap { + height: $van-tabs-line-height; + } + } + + &--card { + padding-top: $van-tabs-card-height; + + .van-tabs__wrap { + height: $van-tabs-card-height; + } + } +} + +.van-tab { + flex: 1; + cursor: pointer; + padding: 0 5px; + font-size: 14px; + position: relative; + color: $text-color; + line-height: $van-tabs-line-height; + text-align: center; + box-sizing: border-box; + background-color: $white; + min-width: 0; /* hack for flex ellipsis */ + + span { + display: block; + } + + &:active { + background-color: $active-color; + } + + &--active { + color: $red; + } + + &--disabled { + color: $gray; + + &:active { + background-color: $white; + } + } +} diff --git a/packages/tabs/index.wxml b/packages/tabs/index.wxml new file mode 100644 index 00000000..906d1977 --- /dev/null +++ b/packages/tabs/index.wxml @@ -0,0 +1,26 @@ + + + + + + + {{ item.data.title }} + + + + + + + +