From 988e2844c0d4e10b524c85b9540b0e295335ebae Mon Sep 17 00:00:00 2001 From: doly mood Date: Fri, 29 Jun 2018 15:50:04 +0800 Subject: [PATCH] Feat Sticky, ScrollNavBar and ScrollNav (#237) * feat(mixins): add new parent minxin creator * feat(sticky): init sticky component * feat(scroll-nav-bar): init logic * feat(scroll-nav): init logic * feat(modules): add sticky, scroll-nav, scroll-nav-bar * test(sticky): init tests * test(scroll-bar-bar): init tests * test(scroll-nav): init tests * docs(scroll-nav-bar): init doc * docs(sticky): init doc * docs(scroll-nav): init docs * docs(entry): add sticky, scroll-nav-bar, scroll-nav entries * chore(package.json): better-scroll to 1.12.4 * chore(yarn): yarn update --- document/common/config/menu.json | 10 +- .../components/docs/en-US/scroll-nav-bar.md | 132 ++++++++ document/components/docs/en-US/scroll-nav.md | 176 ++++++++++ document/components/docs/en-US/sticky.md | 299 +++++++++++++++++ .../components/docs/zh-CN/scroll-nav-bar.md | 130 ++++++++ document/components/docs/zh-CN/scroll-nav.md | 176 ++++++++++ document/components/docs/zh-CN/sticky.md | 303 ++++++++++++++++++ example/App.vue | 12 + example/data/goods-list.json | 2 +- example/pages/scroll-nav-bar.vue | 67 ++++ example/pages/scroll-nav/default.vue | 84 +++++ example/pages/scroll-nav/index.vue | 33 ++ example/pages/scroll-nav/side.vue | 116 +++++++ example/pages/sticky/index.vue | 34 ++ example/pages/sticky/native.vue | 109 +++++++ example/pages/sticky/scroll.vue | 113 +++++++ example/pages/sticky/wechat.vue | 119 +++++++ example/router/routes.js | 44 +++ package-lock.json | 6 +- package.json | 2 +- src/common/mixins/parent.js | 22 ++ src/common/stylus/theme/default.styl | 5 + .../scroll-nav-bar/scroll-nav-bar.vue | 169 ++++++++++ .../scroll-nav/scroll-nav-panel.vue | 42 +++ src/components/scroll-nav/scroll-nav.vue | 234 ++++++++++++++ src/components/scroll/scroll.vue | 3 + src/components/sticky/sticky-ele.vue | 43 +++ src/components/sticky/sticky.vue | 204 ++++++++++++ src/index.js | 8 +- src/module.js | 10 + src/modules/scroll-nav-bar/index.js | 7 + src/modules/scroll-nav/index.js | 14 + src/modules/sticky/index.js | 11 + test/unit/specs/scroll-nav-bar.spec.js | 114 +++++++ test/unit/specs/scroll-nav.spec.js | 191 +++++++++++ test/unit/specs/sticky.spec.js | 242 ++++++++++++++ yarn.lock | 6 +- 37 files changed, 3281 insertions(+), 11 deletions(-) create mode 100644 document/components/docs/en-US/scroll-nav-bar.md create mode 100644 document/components/docs/en-US/scroll-nav.md create mode 100644 document/components/docs/en-US/sticky.md create mode 100644 document/components/docs/zh-CN/scroll-nav-bar.md create mode 100644 document/components/docs/zh-CN/scroll-nav.md create mode 100644 document/components/docs/zh-CN/sticky.md create mode 100644 example/pages/scroll-nav-bar.vue create mode 100644 example/pages/scroll-nav/default.vue create mode 100644 example/pages/scroll-nav/index.vue create mode 100644 example/pages/scroll-nav/side.vue create mode 100644 example/pages/sticky/index.vue create mode 100644 example/pages/sticky/native.vue create mode 100644 example/pages/sticky/scroll.vue create mode 100644 example/pages/sticky/wechat.vue create mode 100644 src/common/mixins/parent.js create mode 100644 src/components/scroll-nav-bar/scroll-nav-bar.vue create mode 100644 src/components/scroll-nav/scroll-nav-panel.vue create mode 100644 src/components/scroll-nav/scroll-nav.vue create mode 100644 src/components/sticky/sticky-ele.vue create mode 100644 src/components/sticky/sticky.vue create mode 100644 src/modules/scroll-nav-bar/index.js create mode 100644 src/modules/scroll-nav/index.js create mode 100644 src/modules/sticky/index.js create mode 100644 test/unit/specs/scroll-nav-bar.spec.js create mode 100644 test/unit/specs/scroll-nav.spec.js create mode 100644 test/unit/specs/sticky.spec.js diff --git a/document/common/config/menu.json b/document/common/config/menu.json index 015ace191..d7e0c04cb 100644 --- a/document/common/config/menu.json +++ b/document/common/config/menu.json @@ -63,7 +63,10 @@ "scroll": "Scroll", "slide": "Slide", "index-list": "IndexList", - "swipe": "Swipe" + "swipe": "Swipe", + "sticky": "Sticky", + "scroll-nav-bar": "ScrollNavBar", + "scroll-nav": "ScrollNav" } } } @@ -142,7 +145,10 @@ "scroll": "Scroll", "slide": "Slide", "index-list": "IndexList", - "swipe": "Swipe" + "swipe": "Swipe", + "sticky": "Sticky", + "scroll-nav-bar": "ScrollNavBar", + "scroll-nav": "ScrollNav" } } } diff --git a/document/components/docs/en-US/scroll-nav-bar.md b/document/components/docs/en-US/scroll-nav-bar.md new file mode 100644 index 000000000..a2ec04b14 --- /dev/null +++ b/document/components/docs/en-US/scroll-nav-bar.md @@ -0,0 +1,132 @@ +## ScrollNavBar + +> New in 1.10.0+ + +Scroll navigation bar component, like DiDi business navigation. + +### Example + +- Default horizontal + + ```html + + ``` + ```js + export default { + data() { + return { + current: '快车', + labels: [ + '快车', + '小巴', + '专车', + '顺风车', + '代驾', + '公交', + '自驾租车', + '豪华车', + '二手车', + '出租车' + ] + } + }, + methods: { + changeHandler(cur) { + this.current = cur + } + } + } + ``` + + You can set the active item by `current` Prop. `labels` is the collection if all the item's keys. The `change` events will be triggered when active item changed. + +- Vertical + + ```html +
+ + {{props.txt}} + +
+ ``` + ```js + export default { + data() { + return { + current: '快车', + labels: [ + '快车', + '小巴', + '专车', + '顺风车', + '代驾', + '公交', + '自驾租车', + '豪华车', + '二手车', + '出租车' + ], + txts: [ + '1快车', + '2小巴', + '3专车', + '4顺风车', + '5代驾', + '6公交', + '7自驾租车', + '8豪华车', + '9二手车', + '10出租车' + ] + } + }, + methods: { + changeHandler(cur) { + this.current = cur + } + } + } + ``` + ```stylus + .side-container + height: 300px + margin-top: 20px + ``` + + If the `direction` Prop is `vertical` then the ScrollNavBar will be vertical style. + + You can use `txts` Prop to defined the text of showcases, it's order is correspondence with `labels` Prop. The default `txts` value is `labels`. + + You can also use default scoped slot to defined your own item content. + +### Props + +| Attribute | Description | Type | Accepted Values | Default | +| - | - | - | - | - | +| direction | direction, default horizontal | String | horizontal/vertical | horizontal | +| labels | the collection if all the item's keys | Array | - | [] | +| txts | the text of showcases, it's order is correspondence with `labels` | Array | - | default equals to `labels` Prop | +| current | the key of the active item | String/Number | - | '' | + +### Slot + +| Name | Description | Scope Parameters | +| - | - | - | +| default | default content | txt: item's text
index: item's index
active: the key of active item
label: item's label | + +### Events + +| Event Name | Description | Parameters | +| - | - | - | +| change | triggers when active item changed | active - the key of active item | + +### Instance methods + +| Method name | Description | +| - | - | +| refresh | You can call this method when content updated | diff --git a/document/components/docs/en-US/scroll-nav.md b/document/components/docs/en-US/scroll-nav.md new file mode 100644 index 000000000..6c50b7da8 --- /dev/null +++ b/document/components/docs/en-US/scroll-nav.md @@ -0,0 +1,176 @@ +## ScrollNav + +> New in 1.10.0+ + +ScrollNav component. You can use it to sticky your navigation bar and content. + +### Example + +- Basic usage - Default + + ```html + + +
    +
  • +
    + +

    {{food.name}}

    +
    +
  • +
+
+
+ ``` + ```js + import goodsData from 'example/data/goods-list.json' + + const goods = goodsData.goods + + export default { + data() { + return { + data: goods + } + }, + methods: { + changeHandler(label) { + console.log('changed to:', label) + } + } + } + ``` + + `goods`: + ```js + "goods": [ + { + "name": "热销榜", + "type": -1, + "foods": [ + { + "name": "皮蛋瘦肉粥", + // ... + "icon": "http://fuss10.elemecdn.com/c/cd/c12745ed8a5171e13b427dbc39401jpeg.jpeg?imageView2/1/w/114/h/114", + "image": "http://fuss10.elemecdn.com/c/cd/c12745ed8a5171e13b427dbc39401jpeg.jpeg?imageView2/1/w/750/h/750" + }, + // ... + ] + }, + // ... + ] + ``` + + `cube-scroll-nav` should contains `cube-scroll-nav-panel` which is the navigation target panel content. + + `cube-scroll-nav-panel` required Prop: `label`(the key of panel). + + The `change` event will be triggered when navigation active item changed. + +- Side Style + + ```html + +
    +
  • 11
  • +
  • 22
  • +
  • 333
  • +
+ +
    +
  • +
    + +

    {{food.name}}

    +
    +
  • +
+
+
+ ``` + ```js + import goodsData from 'example/data/goods-list.json' + + const goods = goodsData.goods + + export default { + components: { + CubePage + }, + data() { + return { + data: goods, + current: goods[3].name + } + }, + methods: { + changeHandler(label) { + console.log('changed to:', label) + }, + stickyChangeHandler(current) { + console.log('sticky-change', current) + } + } + } + ``` + + If set `side-style` to `true` then the navigation bar will be at the side position. + + `data` is the data source, it will be passed to `cube-scroll` component, if the `data` updated then `cube-scroll` will be auto refreshed. + + `current` is the default navigation value(label of panel), if set this value then the target panel will be auto scrolled to the top of container. + + The `sticky-change` event will be triggered when the navigation bar's sticky state changed. The parameter is one of `''`, `'cube-scroll-nav-bar'`. + + You can use `prepend` slot to insert your own content before the main content. + +### Props + +#### CubeScrollNav + +| Attribute | Description | Type | Accepted Values | Default | +| - | - | - | - | - | +| data | optional, data source | Array | - | - | +| sideStyle | If set to `true` then the navigation bar will be at the side position | Boolean | true/false | false | +| current | the default navigation value(label of panel) | String/Number | - | '' | +| speed | speed of navigating to target panel | Number | - | 300 | + +#### CubeScrollNavPanel + +| Attribute | Description | Type | Accepted Values | Default | +| - | - | - | - | - | +| label | required, the key of panel | String/Number | - | - | +| title | the title of panel | String/Number | - | default equal to `label` Prop | + +### Slot + +| Name | Description | Scope Parameters | +| - | - | - | +| default | default panel content | - | +| prepend | prepend content | - | +| bar | navigation bar content, you should use `cube-scroll-nav-bar` component to define your own navigation bar content | labels: the collection of all panel labels
txts: the collection of all panel titles
current: current active navigation value(label of panel) | + +### Events + +| Event Name | Description | Parameters | +| - | - | - | +| change | triggers when navigation active item changed | active - active navigation value(label of panel) | +| sticky-change | triggers when the navigation bar's sticky state changed | current - if navigation bar is fixed then this value will be 'cube-scroll-nav-bar', otherwise this value will be '' | + +### Instance methods + +| Method name | Description | +| - | - | +| refresh | You can call this method when content updated | diff --git a/document/components/docs/en-US/sticky.md b/document/components/docs/en-US/sticky.md new file mode 100644 index 000000000..16b16c5c5 --- /dev/null +++ b/document/components/docs/en-US/sticky.md @@ -0,0 +1,299 @@ +## Sticky + +> New in 1.10.0+ + +Sticky component, the element will be sticky when the scroll position matched the target elements position. + +### Example + +- Default usage - Scroll + + ```html + + +
    +
  • title
  • +
+ + + +
    +
  • {{item}}
  • +
+ + + +
    +
  • {{item}}
  • +
+ + + +
    +
  • {{item}}
  • +
+
+ +
+ ``` + ```js + const _data = [ + '😀 😁 😂 🤣 😃 😄 ', + '🙂 🤗 🤩 🤔 🤨 😐 ', + '👆🏻 scroll up/down 👇🏻 ', + '😔 😕 🙃 🤑 😲 ☹️ ', + '🐣 🐣 🐣 🐣 🐣 🐣 ', + '👆🏻 scroll up/down 👇🏻 ', + '🐥 🐥 🐥 🐥 🐥 🐥 ', + '🤓 🤓 🤓 🤓 🤓 🤓 ', + '👆🏻 scroll up/down 👇🏻 ', + '🦔 🦔 🦔 🦔 🦔 🦔 ', + '🙈 🙈 🙈 🙈 🙈 🙈 ', + '👆🏻 scroll up/down 👇🏻 ', + '🚖 🚖 🚖 🚖 🚖 🚖 ', + '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ' + ] + export default { + data() { + return { + scrollEvents: ['scroll'], + scrollY: 0, + items: _data.concat(), + items2: _data.concat(), + items3: _data.concat() + } + }, + methods: { + scrollHandler({ y }) { + this.scrollY = -y + } + } + } + ``` + + `cube-sticky` need to use with `cube-sticky-ele` component. + + The `pos` Prop of `cube-sticky` component is the scroll runtime position value. + + The `ele-key` Prop of `cube-sticky-ele` component is optional, it is used to define the key value of sticky ele. The default `ele-key` value is the current sticky ele's index value. + + You can alse use `cube-sticky`'s `fixed` slot to define your own sticky HTML. If you do not use `fixed` slot then the default sticky content will be the active `cube-sticky-ele`'s content. + +- Default usage - Native Scroll + + ```html + +
+
    +
  • title
  • +
+ + + +
    +
  • {{item}}
  • +
+ + + +
    +
  • {{item}}
  • +
+ + + +
    +
  • {{item}}
  • +
+
+
+ ``` + ```js + const _data = [ + '😀 😁 😂 🤣 😃 😄 ', + '🙂 🤗 🤩 🤔 🤨 😐 ', + '👆🏻 scroll up/down 👇🏻 ', + '😔 😕 🙃 🤑 😲 ☹️ ', + '🐣 🐣 🐣 🐣 🐣 🐣 ', + '👆🏻 scroll up/down 👇🏻 ', + '🐥 🐥 🐥 🐥 🐥 🐥 ', + '🤓 🤓 🤓 🤓 🤓 🤓 ', + '👆🏻 scroll up/down 👇🏻 ', + '🦔 🦔 🦔 🦔 🦔 🦔 ', + '🙈 🙈 🙈 🙈 🙈 🙈 ', + '👆🏻 scroll up/down 👇🏻 ', + '🚖 🚖 🚖 🚖 🚖 🚖 ', + '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ' + ] + + export default { + data() { + return { + scrollY: 0, + checkTop: false, + items: _data.concat(), + items2: _data.concat(), + items3: _data.concat() + } + }, + methods: { + scrollHandler(e) { + this.scrollY = e.currentTarget.scrollTop + } + } + } + ``` + ```stylus + .scroll-ele + height: 100% + overflow: auto + -webkit-overflow-scrolling: touch + background-color: #fff + ``` + + `check-top` Prop is used to control the checking of boundary conditions, it's default value is `true`, this means `cube-sticky-ele` element will be sticky when the top of the `cube-sticky-ele` reaches the top of `cube-sticky`. This case the `check-top` is `false` which means `cube-sticky-ele` element will be sticky when the bottom of the `cube-sticky-ele` reaches the top of `cube-sticky`. + +- Like WeChat + + ```html + + + + + + +
    +
  • {{item}}
  • +
+
    +
  • {{item}}
  • +
+
    +
  • {{item}}
  • +
+
+ +
+ ``` + ```js + const _data = [ + '😀 😁 😂 🤣 😃 😄 ', + '🙂 🤗 🤩 🤔 🤨 😐 ', + '👆🏻 scroll up/down 👇🏻 ', + '😔 😕 🙃 🤑 😲 ☹️ ', + '🐣 🐣 🐣 🐣 🐣 🐣 ', + '👆🏻 scroll up/down 👇🏻 ', + '🐥 🐥 🐥 🐥 🐥 🐥 ', + '🤓 🤓 🤓 🤓 🤓 🤓 ', + '👆🏻 scroll up/down 👇🏻 ', + '🦔 🦔 🦔 🦔 🦔 🦔 ', + '🙈 🙈 🙈 🙈 🙈 🙈 ', + '👆🏻 scroll up/down 👇🏻 ', + '🚖 🚖 🚖 🚖 🚖 🚖 ', + '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ' + ] + + export default { + data() { + return { + scrollEvents: ['scroll'], + scrollY: 0, + checkTop: true, + items: _data.concat(), + items2: _data.concat(), + items3: _data.concat() + } + }, + methods: { + scrollHandler({ y }) { + this.scrollY = -y + }, + diffChange() { + let opacity = 0 + if (height) { + opacity = diff / height + } + this.$refs.stickyHeader.style.opacity = opacity + } + } + } + ``` + ```stylus + .sticky-fixed-show-enter, .sticky-fixed-show-leave-active + transform: translate(0, -100%) + .sticky-fixed-show-enter-active, .sticky-fixed-show-leave-active + transition: all .5s ease-in-out + ``` + + `fixed-show-ani` is used to set the `transition` name of the sticky element when it's fixed. + + You can get current sticky ele's fixed diff value on `diff-change` event. + +### Props + +#### CubeSticky + +| Attribute | Description | Type | Accepted Values | Default | +| - | - | - | - | - | +| pos | required, scroll position | Number | - | - | +| checkTop | whether to treat the top of sticky element as boundary, if this value is false means `cube-sticky-ele` will be sticky when the bottom of the `cube-sticky-ele` reaches the top of `cube-sticky` | Boolean | true/false | true | +| fixedShowAni | the `transition` name of the sticky element | String | - | if checkTop is true this value will be '', otherwise this value will be 'cube-sticky-fixed-fade' | +| offset | offset value, `pos+offset` will be the real pos value | Number | - | 0 | + +#### CubeStickyEle + +| Attribute | Description | Type | Accepted Values | Default | +| - | - | - | - | - | +| eleKey | the key of sticky element | Number/String | - | - | + +If `eleKey` is undefined, CubeSticky will get the index of CubeStickyEle as the element's key. + +### Slot + +| Name | Description | Scope Parameters | +| - | - | - | +| default | default content | - | +| fixed | custom sticky content when the sticky element is fixed | current: key of the fixed sticky element
index: index of the sticky element(index is not reactive) | + +### Events + +| Event Name | Description | Parameters 1 | Parameters 2 | +| - | - | - | - | +| change | triggers when fixed sticky element changed | current - key of the fixed sticky element, if there is no fixed sticky element this value will be '' | index - index of sticky element, if there is no fixed sticky element this value will be -1 (index is not reactive) | +| diff-change | triggers when sticky element scrolling diff value changed | diff - diff value | height: height of current sticky element | + +### Instance methods + +| Method name | Description | +| - | - | +| refresh | You can call this method when content updated | diff --git a/document/components/docs/zh-CN/scroll-nav-bar.md b/document/components/docs/zh-CN/scroll-nav-bar.md new file mode 100644 index 000000000..057358bc6 --- /dev/null +++ b/document/components/docs/zh-CN/scroll-nav-bar.md @@ -0,0 +1,130 @@ +## ScrollNavBar 组件 + +> 1.10.0 新增 + +滚动导航条组件,效果类似于滴滴打车业务线切换。 + +### 示例 + +- 横向,默认 + + ```html + + ``` + ```js + export default { + data() { + return { + current: '快车', + labels: [ + '快车', + '小巴', + '专车', + '顺风车', + '代驾', + '公交', + '自驾租车', + '豪华车', + '二手车', + '出租车' + ] + } + }, + methods: { + changeHandler(cur) { + this.current = cur + } + } + } + ``` + + 可以通过 `current` 指定 active 的 key 值,`labels` 则是所有的 key 值的集合;当 active 的值发生变化后,会触发 `change` 事件。 + +- 竖向 + + ```html +
+ + {{props.txt}} + +
+ ``` + ```js + export default { + data() { + return { + current: '快车', + labels: [ + '快车', + '小巴', + '专车', + '顺风车', + '代驾', + '公交', + '自驾租车', + '豪华车', + '二手车', + '出租车' + ], + txts: [ + '1快车', + '2小巴', + '3专车', + '4顺风车', + '5代驾', + '6公交', + '7自驾租车', + '8豪华车', + '9二手车', + '10出租车' + ] + } + }, + methods: { + changeHandler(cur) { + this.current = cur + } + } + } + ``` + ```stylus + .side-container + height: 300px + margin-top: 20px + ``` + + 这是一个竖向的示例,`direction` 设置为 `vertical` 代表是一个竖向导航条样式;这里还使用了 `txts` Prop 用于指定展示文案,顺序是和 `labels` 一一对应的,如果没传入 `txts`,则默认 `txts` 就等于 `labels`。 + + 该示例中还展示了默认作用域插槽的使用,`props.txt` 就是当前项的 txt 的值。 + +### Props + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| - | - | - | - | - | +| direction | 方向,默认横向,horizontal | String | horizontal/vertical | horizontal | +| labels | 所有的 key 值集合 | Array | - | [] | +| txts | 显示所有文案集合,和 labels 一一对应 | Array | - | 默认等于 labes | +| current | 当前 active 的 key 值 | String/Number | - | '' | + +### 插槽 + +| 名字 | 说明 | 作用域参数 | +| - | - | - | +| default | 默认内容 | txt: 当前项文案 txt 的值
index: 当前索引值
active: 当前 active 的 key 值
label: 当前项 label 值,即 key 值 | + +### 事件 + +| 事件名 | 说明 | 参数 | +| - | - | - | +| change | 当 active 项发生改变的时候触发 | active - 当前 active 的 key 值 | + +### 实例方法 + +| 方法名 | 说明 | +| - | - | +| refresh | 刷新,当内容发生变化时可用于刷新 scroll 以及重新适应位置 | diff --git a/document/components/docs/zh-CN/scroll-nav.md b/document/components/docs/zh-CN/scroll-nav.md new file mode 100644 index 000000000..c6a5d9910 --- /dev/null +++ b/document/components/docs/zh-CN/scroll-nav.md @@ -0,0 +1,176 @@ +## ScrollNav 组件 + +> 1.10.0 新增 + +滚动导航组件。 + +### 示例 + +- 基本使用 - Default + + ```html + + +
    +
  • +
    + +

    {{food.name}}

    +
    +
  • +
+
+
+ ``` + ```js + import goodsData from 'example/data/goods-list.json' + + const goods = goodsData.goods + + export default { + data() { + return { + data: goods + } + }, + methods: { + changeHandler(label) { + console.log('changed to:', label) + } + } + } + ``` + + `goods` 的结构类似于这样: + ```js + "goods": [ + { + "name": "热销榜", + "type": -1, + "foods": [ + { + "name": "皮蛋瘦肉粥", + // ... + "icon": "http://fuss10.elemecdn.com/c/cd/c12745ed8a5171e13b427dbc39401jpeg.jpeg?imageView2/1/w/114/h/114", + "image": "http://fuss10.elemecdn.com/c/cd/c12745ed8a5171e13b427dbc39401jpeg.jpeg?imageView2/1/w/750/h/750" + }, + // ... + ] + }, + // ... + ] + ``` + + `cube-scroll-nav` 需要和 `cube-scroll-nav-panel` 组件配合使用,`cube-scroll-nav-panel` 就是每一个需要导航定位到的面板组件。 + + `cube-scroll-nav-panel` 组件需要一个必须的 Prop:`label`,唯一标示 key 值。 + + 当导航 active 项发生变化的时候就会触发 `change` 事件。 + +- 侧边式 - Side Style + + ```html + +
    +
  • 11
  • +
  • 22
  • +
  • 333
  • +
+ +
    +
  • +
    + +

    {{food.name}}

    +
    +
  • +
+
+
+ ``` + ```js + import goodsData from 'example/data/goods-list.json' + + const goods = goodsData.goods + + export default { + components: { + CubePage + }, + data() { + return { + data: goods, + current: goods[3].name + } + }, + methods: { + changeHandler(label) { + console.log('changed to:', label) + }, + stickyChangeHandler(current) { + console.log('sticky-change', current) + } + } + } + ``` + + `side-style` 设置为 `true` 代表是一个侧边样式效果; + + `data` 则是默认传入的数据,这个数据会被传入内部使用的 `cube-scroll` 组件,当 `data` 发生变化时,scroll 组件会自动刷新; + + `current` 是导航默认值,如果有值则会默认滚动到相对应的面板 Panel 的位置。 + + `sticky-change` 就是当前导航条的吸附状态发生改变的时候触发,参数可能的值目前只有两个,一个是 `''` 一个是 `'cube-scroll-nav-bar'`。 + + 此外,这里还在 `cube-scroll-nav-panel` 之前插入了一些内容,利用 `prepend` 插槽实现。 + +### Props + +#### CubeScrollNav + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| - | - | - | - | - | +| data | 数据源,可选 | Array | - | - | +| sideStyle | 是否是侧边样式 | Boolean | true/false | false | +| current | 当前导航 active 项的 key 值 | String/Number | - | '' | +| speed | 点击导航切换到指定位置的速度 | Number | - | 300 | + +#### CubeScrollNavPanel + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| - | - | - | - | - | +| label | 必须,面板的唯一标示的值 | String/Number | - | - | +| title | 面板标题内容 | String/Number | - | 默认等于 label | + +### 插槽 + +| 名字 | 说明 | 作用域参数 | +| - | - | - | +| default | 默认面板内容 | - | +| prepend | 前置内容,在导航之前 | - | +| bar | 导航内容,如果需要自定义导航结构的话使用,里边放置 cube-scroll-nav-bar 组件 | labels: 所有面板的 label 值集合
txts: 所有面板的 title 值集合
current: 当前导航 active 项的 key 值 | + +### 事件 + +| 事件名 | 说明 | 参数 | +| - | - | - | +| change | 当导航 active 项发生变化的时候 | active - 当前 active 的 key 值 | +| sticky-change | 当前导航条的吸附状态发生改变的时候触发 | current - 如果未吸附,则值为 '' 否则为 'cube-scroll-nav-bar' | + +### 实例方法 + +| 方法名 | 说明 | +| - | - | +| refresh | 刷新,当内容或高度发生变化时可用于重新计算整体内容、位置 | diff --git a/document/components/docs/zh-CN/sticky.md b/document/components/docs/zh-CN/sticky.md new file mode 100644 index 000000000..d24cdc423 --- /dev/null +++ b/document/components/docs/zh-CN/sticky.md @@ -0,0 +1,303 @@ +## Sticky 组件 + +> 1.10.0 新增 + +吸附组件,当滚动位置到达目标元素位置后,目标元素就会自动吸附。 + +### 示例 + +- 综合使用 - Scroll + + ```html + + +
    +
  • title
  • +
+ + + +
    +
  • {{item}}
  • +
+ + + +
    +
  • {{item}}
  • +
+ + + +
    +
  • {{item}}
  • +
+
+ +
+ ``` + ```js + const _data = [ + '😀 😁 😂 🤣 😃 😄 ', + '🙂 🤗 🤩 🤔 🤨 😐 ', + '👆🏻 scroll up/down 👇🏻 ', + '😔 😕 🙃 🤑 😲 ☹️ ', + '🐣 🐣 🐣 🐣 🐣 🐣 ', + '👆🏻 scroll up/down 👇🏻 ', + '🐥 🐥 🐥 🐥 🐥 🐥 ', + '🤓 🤓 🤓 🤓 🤓 🤓 ', + '👆🏻 scroll up/down 👇🏻 ', + '🦔 🦔 🦔 🦔 🦔 🦔 ', + '🙈 🙈 🙈 🙈 🙈 🙈 ', + '👆🏻 scroll up/down 👇🏻 ', + '🚖 🚖 🚖 🚖 🚖 🚖 ', + '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ' + ] + export default { + data() { + return { + scrollEvents: ['scroll'], + scrollY: 0, + items: _data.concat(), + items2: _data.concat(), + items3: _data.concat() + } + }, + methods: { + scrollHandler({ y }) { + this.scrollY = -y + } + } + } + ``` + + `cube-sticky` 需要和 `cube-sticky-ele` 组件配合使用。 + + `cube-sticky` 组件依赖 `pos` Prop 来指定当前实时位置,由于 Scroll 组件中派发的 `y` 为负值,所以我们需要将其转换为滚动位置,直接取 `-y` 即可。 + + `cube-sticky-ele` 组件的 `ele-key` Prop 是可选的,用于指定当前吸附元素的 key 值,默认为当前组件的索引值,即在 `cube-sticky` 组件中的第几个 `cube-sticky-ele`。 + + 我们还可以利用 `cube-sticky` 组件的 `fixed` 插槽实现自定义吸附效果,如果没有插槽的话,内部默认会将吸附的 `cube-sticky-ele` 的内容显示出来。 + +- 综合使用 - Native Scroll + + 利用原生滚动实现效果。 + + ```html + +
+
    +
  • title
  • +
+ + + +
    +
  • {{item}}
  • +
+ + + +
    +
  • {{item}}
  • +
+ + + +
    +
  • {{item}}
  • +
+
+
+ ``` + ```js + const _data = [ + '😀 😁 😂 🤣 😃 😄 ', + '🙂 🤗 🤩 🤔 🤨 😐 ', + '👆🏻 scroll up/down 👇🏻 ', + '😔 😕 🙃 🤑 😲 ☹️ ', + '🐣 🐣 🐣 🐣 🐣 🐣 ', + '👆🏻 scroll up/down 👇🏻 ', + '🐥 🐥 🐥 🐥 🐥 🐥 ', + '🤓 🤓 🤓 🤓 🤓 🤓 ', + '👆🏻 scroll up/down 👇🏻 ', + '🦔 🦔 🦔 🦔 🦔 🦔 ', + '🙈 🙈 🙈 🙈 🙈 🙈 ', + '👆🏻 scroll up/down 👇🏻 ', + '🚖 🚖 🚖 🚖 🚖 🚖 ', + '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ' + ] + + export default { + data() { + return { + scrollY: 0, + checkTop: false, + items: _data.concat(), + items2: _data.concat(), + items3: _data.concat() + } + }, + methods: { + scrollHandler(e) { + this.scrollY = e.currentTarget.scrollTop + } + } + } + ``` + ```stylus + .scroll-ele + height: 100% + overflow: auto + -webkit-overflow-scrolling: touch + background-color: #fff + ``` + + 这个示例展示的就是利用原生滚动实现吸附效果的。 + + `check-top` Prop 则是用来控制检查条件的,默认为 `true`,意味着当 `cube-sticky-ele` 元素的顶部达到 `cube-sticky` 的顶部的时候就会被吸附;这个例子中我们设置为了 `false`,意味着当 `cube-sticky-ele` 的底部达到 `cube-sticky` 的顶部的时候会被吸附。 + +- 模拟微信效果 - WeChat + + ```html + + + + + + +
    +
  • {{item}}
  • +
+
    +
  • {{item}}
  • +
+
    +
  • {{item}}
  • +
+
+ +
+ ``` + ```js + const _data = [ + '😀 😁 😂 🤣 😃 😄 ', + '🙂 🤗 🤩 🤔 🤨 😐 ', + '👆🏻 scroll up/down 👇🏻 ', + '😔 😕 🙃 🤑 😲 ☹️ ', + '🐣 🐣 🐣 🐣 🐣 🐣 ', + '👆🏻 scroll up/down 👇🏻 ', + '🐥 🐥 🐥 🐥 🐥 🐥 ', + '🤓 🤓 🤓 🤓 🤓 🤓 ', + '👆🏻 scroll up/down 👇🏻 ', + '🦔 🦔 🦔 🦔 🦔 🦔 ', + '🙈 🙈 🙈 🙈 🙈 🙈 ', + '👆🏻 scroll up/down 👇🏻 ', + '🚖 🚖 🚖 🚖 🚖 🚖 ', + '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ' + ] + + export default { + data() { + return { + scrollEvents: ['scroll'], + scrollY: 0, + checkTop: true, + items: _data.concat(), + items2: _data.concat(), + items3: _data.concat() + } + }, + methods: { + scrollHandler({ y }) { + this.scrollY = -y + }, + diffChange() { + let opacity = 0 + if (height) { + opacity = diff / height + } + this.$refs.stickyHeader.style.opacity = opacity + } + } + } + ``` + ```stylus + .sticky-fixed-show-enter, .sticky-fixed-show-leave-active + transform: translate(0, -100%) + .sticky-fixed-show-enter-active, .sticky-fixed-show-leave-active + transition: all .5s ease-in-out + ``` + + 可以通过 `fixed-show-ani` 指定当元素吸附时出现的 `transition` 名字,我们这里指定 `sticky-fixed-show`,所以相对应的我们在样式中加了对应的动画控制效果。 + + 同时可以通过 `diff-change` 事件得到当前 sticky 元素滚动的差值,一般我们可以和当前 sticky 元素的高做除法得到相对百分比,可以精细控制出现的具体细节。 + +### Props + +#### CubeSticky + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| - | - | - | - | - | +| pos | 必须,滚动位置 | Number | - | - | +| checkTop | 是否检测顶部位置,如果为 false,则检查 sticky-ele 元素的底部边界 | Boolean | true/false | true | +| fixedShowAni | 元素吸附时指定 transition 的 name 值 | String | - | 如果 checkTop 为 true,则为 '',如果为 false,则为 'cube-sticky-fixed-fade' | +| offset | 偏移值,传入的 pos 的值会加上这个值修正 | Number | - | 0 | + +#### CubeStickyEle + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| - | - | - | - | - | +| eleKey | 吸顶元素的 key 值 | Number/String | - | - | + +当 `eleKey` 不存在时,CubeSticky 组件会获取当前 CubeStickyEle 组件的次序索引作为吸附元素的标示。 + +### 插槽 + +| 名字 | 说明 | 作用域参数 | +| - | - | - | +| default | 默认内容 | - | +| fixed | 元素吸附时内容如何展示 | current: 当前吸附元素的 key 值
index: 吸附元素索引值(非响应式的) | + +### 事件 + +| 事件名 | 说明 | 参数1 | 参数2 | +| - | - | - | - | +| change | 吸附的元素发生改变时触发 | current - 吸附的元素的 key 值,如果没有的话,则为 '' | index - 吸附元素的索引值,没有的话,则为 -1(非响应式的) | +| diff-change | sticky 元素滚动的差值改变时触发 | diff - 差值,最小 0 | height: 当前 sticky 元素的高度 | + +### 实例方法 + +| 方法名 | 说明 | +| - | - | +| refresh | 刷新,当内容发生变化时可用于重新计算高度和位置 | diff --git a/example/App.vue b/example/App.vue index be2086be4..b21ffc3f7 100644 --- a/example/App.vue +++ b/example/App.vue @@ -174,6 +174,18 @@ { path: '/swipe', text: 'Swipe' + }, + { + path: '/sticky', + text: 'Sticky' + }, + { + path: '/scroll-nav-bar', + text: 'ScrollNavBar' + }, + { + path: '/scroll-nav', + text: 'ScrollNav' } ] } diff --git a/example/data/goods-list.json b/example/data/goods-list.json index ee8d7715c..d834c65c4 100644 --- a/example/data/goods-list.json +++ b/example/data/goods-list.json @@ -1376,4 +1376,4 @@ "recommend": [] } ] -} \ No newline at end of file +} diff --git a/example/pages/scroll-nav-bar.vue b/example/pages/scroll-nav-bar.vue new file mode 100644 index 000000000..158651966 --- /dev/null +++ b/example/pages/scroll-nav-bar.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/example/pages/scroll-nav/default.vue b/example/pages/scroll-nav/default.vue new file mode 100644 index 000000000..df845a15b --- /dev/null +++ b/example/pages/scroll-nav/default.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/example/pages/scroll-nav/index.vue b/example/pages/scroll-nav/index.vue new file mode 100644 index 000000000..f4aca2aae --- /dev/null +++ b/example/pages/scroll-nav/index.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/example/pages/scroll-nav/side.vue b/example/pages/scroll-nav/side.vue new file mode 100644 index 000000000..c52aa9a5c --- /dev/null +++ b/example/pages/scroll-nav/side.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/example/pages/sticky/index.vue b/example/pages/sticky/index.vue new file mode 100644 index 000000000..0f32e3b36 --- /dev/null +++ b/example/pages/sticky/index.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/example/pages/sticky/native.vue b/example/pages/sticky/native.vue new file mode 100644 index 000000000..6fa31fa05 --- /dev/null +++ b/example/pages/sticky/native.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/example/pages/sticky/scroll.vue b/example/pages/sticky/scroll.vue new file mode 100644 index 000000000..ce6e32e77 --- /dev/null +++ b/example/pages/sticky/scroll.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/example/pages/sticky/wechat.vue b/example/pages/sticky/wechat.vue new file mode 100644 index 000000000..d936295c2 --- /dev/null +++ b/example/pages/sticky/wechat.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/example/router/routes.js b/example/router/routes.js index 7b12b274a..87ecd456c 100644 --- a/example/router/routes.js +++ b/example/router/routes.js @@ -49,6 +49,14 @@ import Slide from '../pages/slide/index.vue' import SlideVertical from '../pages/slide/vertical.vue' import SlideHorizontal from '../pages/slide/horizontal.vue' import Toolbar from '../pages/toolbar.vue' +import Sticky from '../pages/sticky/index.vue' +import StickyScroll from '../pages/sticky/scroll.vue' +import StickyNative from '../pages/sticky/native.vue' +import StickyWechat from '../pages/sticky/wechat.vue' +import ScrollNavBar from '../pages/scroll-nav-bar.vue' +import ScrollNav from '../pages/scroll-nav/index.vue' +import ScrollNavDefault from '../pages/scroll-nav/default.vue' +import ScrollNavSide from '../pages/scroll-nav/side.vue' import ImagePreview from '../pages/image-preview.vue' import TabBarIndex from '../pages/tab-bar/index.vue' import TabBar from '../pages/tab-bar/tab-bar.vue' @@ -284,6 +292,42 @@ const routes = [ path: '/toolbar', component: Toolbar }, + { + path: '/sticky', + component: Sticky, + children: [ + { + path: 'scroll', + component: StickyScroll + }, + { + path: 'native', + component: StickyNative + }, + { + path: 'wechat', + component: StickyWechat + } + ] + }, + { + path: '/scroll-nav-bar', + component: ScrollNavBar + }, + { + path: '/scroll-nav', + component: ScrollNav, + children: [ + { + path: 'default', + component: ScrollNavDefault + }, + { + path: 'side', + component: ScrollNavSide + } + ] + }, { path: '/tab-bar', component: TabBarIndex, diff --git a/package-lock.json b/package-lock.json index d3983118c..3a82f17b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1613,9 +1613,9 @@ } }, "better-scroll": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/better-scroll/-/better-scroll-1.12.0.tgz", - "integrity": "sha512-GYOith45ic5NIV1+hOAFVlx3M0PcmsZtt7Vj0MLi+wgsVTGxoP38h3pRb5DQzeho4L5Gz+E1xZG5lj/tEhWLMA==", + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/better-scroll/-/better-scroll-1.12.4.tgz", + "integrity": "sha512-3C7iYCnPCBe9aR1EufpU0oyjQvogmZawIm1REZco1LXVNVnGhn26RJR4av73qcEFhD9HEFLOhYkAA0oREsGSRQ==", "requires": { "babel-runtime": "6.26.0" } diff --git a/package.json b/package.json index ca3a8412a..a0c2e1ea8 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ ], "license": "Apache", "dependencies": { - "better-scroll": "^1.12.0" + "better-scroll": "^1.12.4" }, "peerDependencies": { "vue": "^2.5.13" diff --git a/src/common/mixins/parent.js b/src/common/mixins/parent.js new file mode 100644 index 000000000..bee9621ec --- /dev/null +++ b/src/common/mixins/parent.js @@ -0,0 +1,22 @@ +export default function parentMixinCreator (relationKey, targetKey = 'relationParent') { + return { + created() { + this[targetKey] = this._getTargetParent() + }, + destroyed() { + this[targetKey] = null + }, + methods: { + _getTargetParent() { + let p = this.$parent + while (p) { + if (p[relationKey]) { + return p + } + p = p.$parent + } + return null + } + } + } +} diff --git a/src/common/stylus/theme/default.styl b/src/common/stylus/theme/default.styl index fd925aa95..c5ec0abe8 100644 --- a/src/common/stylus/theme/default.styl +++ b/src/common/stylus/theme/default.styl @@ -207,6 +207,11 @@ $drawer-title-bgc := $color-white $drawer-panel-bgc := $color-white $drawer-item-active-bgc := $color-light-grey-opacity +// scroll-nav +$scroll-nav-bgc := $color-white +$scroll-nav-color := $color-grey +$scroll-nav-active-color := $color-orange + // image-preview $image-preview-counter-color := $color-white diff --git a/src/components/scroll-nav-bar/scroll-nav-bar.vue b/src/components/scroll-nav-bar/scroll-nav-bar.vue new file mode 100644 index 000000000..59d9a3953 --- /dev/null +++ b/src/components/scroll-nav-bar/scroll-nav-bar.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/components/scroll-nav/scroll-nav-panel.vue b/src/components/scroll-nav/scroll-nav-panel.vue new file mode 100644 index 000000000..e54ba4a65 --- /dev/null +++ b/src/components/scroll-nav/scroll-nav-panel.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/scroll-nav/scroll-nav.vue b/src/components/scroll-nav/scroll-nav.vue new file mode 100644 index 000000000..bc9bd5111 --- /dev/null +++ b/src/components/scroll-nav/scroll-nav.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/src/components/scroll/scroll.vue b/src/components/scroll/scroll.vue index dae18089d..66e1c2005 100644 --- a/src/components/scroll/scroll.vue +++ b/src/components/scroll/scroll.vue @@ -390,6 +390,9 @@ height: 100% overflow: hidden + .cube-scroll-list-wrapper + overflow: hidden + .cube-pulldown-wrapper position: absolute width: 100% diff --git a/src/components/sticky/sticky-ele.vue b/src/components/sticky/sticky-ele.vue new file mode 100644 index 000000000..db57ad960 --- /dev/null +++ b/src/components/sticky/sticky-ele.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/components/sticky/sticky.vue b/src/components/sticky/sticky.vue new file mode 100644 index 000000000..c97eef7d9 --- /dev/null +++ b/src/components/sticky/sticky.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/src/index.js b/src/index.js index 63dd5c871..3fd77eaaa 100644 --- a/src/index.js +++ b/src/index.js @@ -36,6 +36,9 @@ import { Slide, IndexList, Swipe, + Sticky, + ScrollNav, + ScrollNavBar, // module BetterScroll, createAPI, @@ -82,7 +85,10 @@ const components = [ Scroll, Slide, IndexList, - Swipe + Swipe, + Sticky, + ScrollNav, + ScrollNavBar ] function install(Vue) { diff --git a/src/module.js b/src/module.js index 2c22470af..64d153c16 100644 --- a/src/module.js +++ b/src/module.js @@ -39,6 +39,9 @@ import Scroll from './modules/scroll' import Slide from './modules/slide' import IndexList from './modules/index-list' import Swipe from './modules/swipe' +import Sticky from './modules/sticky' +import ScrollNav from './modules/scroll-nav' +import ScrollNavBar from './modules/scroll-nav-bar' // module import BetterScroll from './modules/better-scroll' @@ -51,6 +54,8 @@ const Radio = RadioGroup.Radio const SwipeItem = Swipe.Item const DrawerPanel = Drawer.Panel const DrawerItem = Drawer.Item +const StickyEle = Sticky.Ele +const ScrollNavPanel = ScrollNav.Panel const Tab = TabBar.Tab const TabPanel = TabPanels.Panel @@ -102,6 +107,11 @@ export { IndexList, SwipeItem, Swipe, + StickyEle, + Sticky, + ScrollNavPanel, + ScrollNav, + ScrollNavBar, // module BetterScroll, createAPI diff --git a/src/modules/scroll-nav-bar/index.js b/src/modules/scroll-nav-bar/index.js new file mode 100644 index 000000000..ceb8a83cd --- /dev/null +++ b/src/modules/scroll-nav-bar/index.js @@ -0,0 +1,7 @@ +import ScrollNavBar from '../../components/scroll-nav-bar/scroll-nav-bar.vue' + +ScrollNavBar.install = function (Vue) { + Vue.component(ScrollNavBar.name, ScrollNavBar) +} + +export default ScrollNavBar diff --git a/src/modules/scroll-nav/index.js b/src/modules/scroll-nav/index.js new file mode 100644 index 000000000..e02bfaf46 --- /dev/null +++ b/src/modules/scroll-nav/index.js @@ -0,0 +1,14 @@ +import ScrollNav from '../../components/scroll-nav/scroll-nav.vue' +import ScrollNavPanel from '../../components/scroll-nav/scroll-nav-panel.vue' +import ScrollNavBar from '../../components/scroll-nav-bar/scroll-nav-bar.vue' + +ScrollNav.install = function (Vue) { + Vue.component(ScrollNav.name, ScrollNav) + Vue.component(ScrollNavPanel.name, ScrollNavPanel) + Vue.component(ScrollNavBar.name, ScrollNavBar) +} + +ScrollNav.Panel = ScrollNavPanel +ScrollNav.Bar = ScrollNavBar + +export default ScrollNav diff --git a/src/modules/sticky/index.js b/src/modules/sticky/index.js new file mode 100644 index 000000000..1a053e8c4 --- /dev/null +++ b/src/modules/sticky/index.js @@ -0,0 +1,11 @@ +import Sticky from '../../components/sticky/sticky.vue' +import StickyEle from '../../components/sticky/sticky-ele.vue' + +Sticky.install = function (Vue) { + Vue.component(Sticky.name, Sticky) + Vue.component(StickyEle.name, StickyEle) +} + +Sticky.Ele = Sticky + +export default Sticky diff --git a/test/unit/specs/scroll-nav-bar.spec.js b/test/unit/specs/scroll-nav-bar.spec.js new file mode 100644 index 000000000..65cf71b64 --- /dev/null +++ b/test/unit/specs/scroll-nav-bar.spec.js @@ -0,0 +1,114 @@ +import Vue from 'vue2' +import ScrollNavBar from '@/modules/scroll-nav-bar' +import instantiateComponent from '@/common/helpers/instantiate-component' +import { dispatchTap } from '../utils/event' + +const current = '快车' +const labels = [ + '快车', + '小巴', + '专车', + '顺风车', + '代驾', + '公交', + '自驾租车', + '豪华车', + '二手车', + '出租车' +] + +describe('ScrollNavBar', () => { + let vm + + afterEach(() => { + if (vm) { + vm.$parent.destroy() + vm = null + } + }) + + it('use', () => { + Vue.use(ScrollNavBar) + expect(Vue.component(ScrollNavBar.name)) + .to.be.a('function') + }) + + it('should render correct contents', (done) => { + vm = createScrollNavBar() + setTimeout(() => { + expect(vm.$el.className) + .to.equal('cube-scroll-nav-bar cube-scroll-nav-bar_horizontal') + const items = vm.$el.querySelectorAll('.cube-scroll-nav-bar-item') + expect(items.length) + .to.equal(10) + expect(items[0].className) + .to.include('cube-scroll-nav-bar-item_active') + vm.$updateProps({ + current: '二手车' + }) + setTimeout(() => { + expect(vm.$el.querySelector('.cube-scroll-nav-bar-item_active').textContent.trim()) + .equal('二手车') + // click one + dispatchTap(vm.$el.querySelectorAll('.cube-scroll-nav-bar-item')[2]) + setTimeout(() => { + expect(vm.$el.querySelector('.cube-scroll-nav-bar-item_active').textContent.trim()) + .equal('专车') + done() + }) + }) + }, 50) + }) + + it('should render correct contents - vertical', (done) => { + vm = createScrollNavBar({ + direction: 'vertical', + current: current, + labels: labels + }) + setTimeout(() => { + vm.refresh() + expect(vm.$el.className) + .to.equal('cube-scroll-nav-bar cube-scroll-nav-bar_vertical') + const items = vm.$el.querySelectorAll('.cube-scroll-nav-bar-item') + expect(items.length) + .to.equal(10) + expect(items[0].className) + .to.include('cube-scroll-nav-bar-item_active') + done() + }, 50) + }) + + it('should trigger events', (done) => { + const changeHandler = sinon.spy() + + vm = createScrollNavBar({ + current: current, + labels: labels + }, { + change: changeHandler + }) + setTimeout(() => { + dispatchTap(vm.$el.querySelectorAll('.cube-scroll-nav-bar-item')[0]) + setTimeout(() => { + expect(changeHandler) + .not.to.be.called + dispatchTap(vm.$el.querySelectorAll('.cube-scroll-nav-bar-item')[2]) + setTimeout(() => { + expect(changeHandler) + .to.be.calledOnce + expect(changeHandler) + .to.be.calledWith('专车') + done() + }) + }) + }, 50) + }) + + function createScrollNavBar(props = { current: current, labels: labels }, events = {}) { + return instantiateComponent(Vue, ScrollNavBar, { + props, + on: events + }) + } +}) diff --git a/test/unit/specs/scroll-nav.spec.js b/test/unit/specs/scroll-nav.spec.js new file mode 100644 index 000000000..4df593c9f --- /dev/null +++ b/test/unit/specs/scroll-nav.spec.js @@ -0,0 +1,191 @@ +import Vue from 'vue2' +import ScrollNav from '@/modules/scroll-nav' +import createVue from '../utils/create-vue' +import { dispatchSwipe, dispatchTap } from '../utils/event' +import { findIndex } from '@/common/helpers/util' +import cityData from '../fake/index-list.json' + +describe('ScrollNav', () => { + let vm + + afterEach(() => { + if (vm) { + vm.$parent.destroy() + vm = null + } + }) + + it('use', () => { + Vue.use(ScrollNav) + expect(Vue.component(ScrollNav.name)) + .to.be.a('function') + }) + + it('should render correct contents', (done) => { + vm = createScrollNav() + setTimeout(() => { + expect(vm.$el.className) + .to.equal('cube-scroll-nav') + // prepend + expect(vm.$el.querySelector('.prepend-header').nextElementSibling.className) + .to.equal('cube-scroll-nav-main') + + const navBarItems = vm.$el.querySelectorAll('.cube-scroll-nav-bar-item') + expect(navBarItems.length) + .to.equal(cityData.length) + expect(navBarItems[0].className) + .to.include('cube-scroll-nav-bar-item_active') + + const panels = vm.$el.querySelectorAll('.cube-scroll-nav-panel') + expect(panels.length) + .to.equal(cityData.length) + expect(panels[0].querySelector('.cube-scroll-nav-panel-title').textContent.trim()) + .to.equal(cityData[0].name) + done() + }, 50) + }) + + it('should render correct contents - sideStyle', (done) => { + vm = createScrollNav({ + sideStyle: true, + current: cityData[1].name + }, {}, false) + setTimeout(() => { + expect(vm.$el.className) + .to.equal('cube-scroll-nav cube-scroll-nav_side') + expect(vm.$el.querySelector('.cube-scroll-nav-bar-item_active').textContent.trim()) + .to.equal(cityData[1].name) + // sticky + expect(vm.$el.querySelector('.cube-sticky > .cube-sticky-fixed .cube-scroll-nav-bar')) + .not.to.be.null + expect(vm.$el.querySelector('.cube-scroll-wrapper > .cube-sticky-fixed .cube-scroll-nav-panel-title').textContent.trim()) + .to.equal(cityData[1].name) + done() + }, 400) + }) + + it('should sticky & nav correctly', function (done) { + this.timeout(10000) + vm = createScrollNav() + setTimeout(() => { + expect(vm.active) + .to.equal(cityData[0].name) + // scroll to city 3 + vm.$parent.current = cityData[2].name + setTimeout(() => { + expect(vm.active) + .to.equal(cityData[2].name) + // click bar + // to city 2 + dispatchTap(vm.$el.querySelectorAll('.cube-scroll-nav-bar-item')[1]) + setTimeout(() => { + expect(vm.active) + .to.equal(cityData[1].name) + // scroll to top 0 + vm.$refs.scroll.scrollTo(0, 0, 50) + setTimeout(() => { + expect(vm.active) + .to.equal(cityData[0].name) + done() + }, 100) + }, 350) + }, 350) + }, 50) + }) + + it('should trigger events', function (done) { + this.timeout(10000) + const changeHandler = sinon.spy() + const stickyChangeHandler = sinon.spy() + vm = createScrollNav({ + sideStyle: true, + current: cityData[1].name + }, { + onChange: changeHandler, + onStickyChange: stickyChangeHandler + }, false) + setTimeout(() => { + expect(stickyChangeHandler) + .to.be.calledOnce + expect(stickyChangeHandler) + .to.be.calledWith('cube-scroll-nav-bar') + const scroller = vm.$el.querySelector('.cube-scroll-wrapper') + dispatchSwipe(scroller, [ + { + pageX: 300, + pageY: 300 + }, + { + pageX: 300, + pageY: 10 + } + ], 100) + var scrollEnd = () => { + vm.$refs.scroll.scroll.off('scrollEnd', scrollEnd) + const targetActive = 'G' + expect(vm.active) + .to.equal(targetActive) + const index = findIndex(cityData, function (item) { + return item.name === targetActive + }) + expect(changeHandler) + .to.have.callCount(index) + done() + } + setTimeout(() => { + vm.$refs.scroll.scroll.on('scrollEnd', scrollEnd) + }, 120) + }, 400) + }) + + function createScrollNav(props = {}, events = {}, showPrepend = true) { + const prepend = ` +
    +
  • 11
  • +
  • 22
  • +
  • 333
  • +
+ ` + const data = { + sideStyle: false, + current: '', + data: cityData + } + Object.keys(props).forEach((k) => { + data[k] = props[k] + }) + return createVue({ + template: ` +
+ + ${showPrepend ? prepend : ''} + +
    +
  • +
    +

    {{city.cityid}}

    +

    {{city.name}}

    +

    {{city.tags}}

    +
    +
  • +
+
+
+
+ `, + data: data, + methods: { + changeHandler(current) { + events.onChange && events.onChange.call(this, current) + }, + stickyChangeHandler(current) { + events.onStickyChange && events.onStickyChange.call(this, current) + } + } + }) + } +}) diff --git a/test/unit/specs/sticky.spec.js b/test/unit/specs/sticky.spec.js new file mode 100644 index 000000000..0bda0f5d3 --- /dev/null +++ b/test/unit/specs/sticky.spec.js @@ -0,0 +1,242 @@ +import Vue from 'vue2' +import Sticky from '@/modules/sticky' +import createVue from '../utils/create-vue' + +describe('Sticky', () => { + let vm + + afterEach(() => { + if (vm) { + vm.$parent.destroy() + vm = null + } + }) + + it('use', () => { + Vue.use(Sticky) + expect(Vue.component(Sticky.name)) + .to.be.a('function') + }) + + it('should render correct contents', (done) => { + vm = createSticky() + expect(vm.$el.className) + .to.equal('cube-sticky') + const eles = vm.$el.querySelectorAll('.cube-sticky-ele') + expect(eles.length) + .to.equal(3) + expect(vm.$el.querySelector('.cube-sticky-fixed').style.display) + .to.equal('none') + setTimeout(() => { + done() + }) + }) + + it('should fixed ele', (done) => { + vm = createSticky() + expect(vm.currentIndex) + .to.equal(-1) + expect(vm.currentKey) + .to.equal('') + setTimeout(() => { + vm.$parent.scrollTo(280) + setTimeout(() => { + expect(vm.currentIndex) + .to.equal(0) + expect(vm.currentKey) + .to.equal(0) + expect(vm.currentDiff) + .to.equal(250) + const fixed = vm.$el.querySelector('.cube-sticky-fixed') + expect(fixed.style.display) + .not.equal('none') + expect(fixed.textContent.trim()) + .to.equal('111') + vm.$parent.scrollTo(500) + setTimeout(() => { + expect(vm.currentIndex) + .to.equal(1) + expect(vm.currentKey) + .to.equal('2') + expect(vm.currentDiff) + .to.equal(20) + vm.$parent.scrollTo(0) + setTimeout(() => { + expect(vm.currentIndex) + .to.equal(-1) + expect(vm.currentKey) + .to.equal('') + expect(vm.currentDiff) + .to.equal(0) + done() + }) + }) + }) + }) + }) + + it('should fixed ele - checkTop', (done) => { + vm = createSticky({ + checkTop: false + }, {}, ` + + `) + expect(vm.currentIndex) + .to.equal(-1) + expect(vm.currentKey) + .to.equal('') + setTimeout(() => { + vm.$parent.scrollTo(280) + setTimeout(() => { + expect(vm.currentIndex) + .to.equal(0) + expect(vm.currentKey) + .to.equal(0) + expect(vm.currentDiff) + .to.equal(220) + const fixed = vm.$el.querySelector('.cube-sticky-fixed') + expect(fixed.style.display) + .not.equal('none') + expect(fixed.textContent.trim()) + .to.equal('0') + vm.$parent.scrollTo(1000) + setTimeout(() => { + expect(vm.currentIndex) + .to.equal(2) + expect(vm.currentKey) + .to.equal('3') + expect(vm.currentDiff) + .to.equal(10) + vm.$parent.scrollTo(0) + setTimeout(() => { + expect(vm.currentIndex) + .to.equal(-1) + expect(vm.currentKey) + .to.equal('') + expect(vm.currentDiff) + .to.equal(0) + done() + }) + }) + }) + }) + }) + + it('should trigger events', (done) => { + const changeHandler = sinon.spy() + const onDiffChange = sinon.spy() + vm = createSticky(undefined, { + onChange: changeHandler, + onDiffChange: onDiffChange + }) + setTimeout(() => { + vm.$parent.scrollTo(280) + setTimeout(() => { + expect(changeHandler) + .to.be.calledOnce + expect(changeHandler) + .to.be.calledWith(0) + expect(onDiffChange) + .to.be.calledOnce + vm.$parent.scrollTo(500) + setTimeout(() => { + expect(changeHandler) + .to.be.calledTwice + expect(changeHandler) + .to.be.calledWith('2') + expect(onDiffChange) + .to.be.calledTwice + done() + }) + }) + }) + }) + + function createSticky(props = { checkTop: true }, events = {}, fixedSlot) { + const _data = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14' + ] + return createVue({ + template: ` +
+ +
+
    +
  • title
  • +
+ +
    +
  • 111
  • +
+
+
    +
  • {{item}}
  • +
+ +
    +
  • 222
  • +
  • 222
  • +
+
+
    +
  • {{item}}
  • +
+ +
    +
  • 333
  • +
+
+
    +
  • {{item}}
  • +
+
+ ${fixedSlot || ''} +
+
+ `, + data: { + checkTop: props.checkTop, + scrollY: 0, + items: _data.concat(), + items2: _data.concat(), + items3: _data.concat() + }, + methods: { + changeHandler(key, index) { + events.onChange && events.onChange.call(this, key, index) + }, + diffChangeHandler(diff, height) { + events.onDiffChange && events.onDiffChange.call(this, diff, height) + }, + scrollHandler(e) { + this.scrollY = e.currentTarget.scrollTop + }, + scrollTo(y) { + this.$refs.scroller.scrollTop = y + } + } + }) + } +}) diff --git a/yarn.lock b/yarn.lock index bfcbaaf5a..735f49a1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1090,9 +1090,9 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" -better-scroll@1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/better-scroll/-/better-scroll-1.12.0.tgz#f15e1e316eb609690de6c733538b2b092aa3b118" +better-scroll@^1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/better-scroll/-/better-scroll-1.12.4.tgz#27d722fe7b6844176feb136da4f2fd39c70f973e" dependencies: babel-runtime "^6.0.0"