Commit 43513f702cd573f8078ca582bc9ac264e707028e

Authored by zhigang.li
1 parent eeeceb54

add anchor component

examples/app.vue
... ... @@ -18,6 +18,7 @@ nav {
18 18 <ul>
19 19 <li><router-link to="/layout">Layout</router-link></li>
20 20 <li><router-link to="/affix">Affix</router-link></li>
  21 + <li><router-link to="/anchor">Anchor</router-link></li>
21 22 <li><router-link to="/grid">Grid</router-link></li>
22 23 <li><router-link to="/button">Button</router-link></li>
23 24 <li><router-link to="/input">Input</router-link></li>
... ...
examples/main.js
... ... @@ -28,6 +28,10 @@ const router = new VueRouter({
28 28 component: (resolve) => require(['./routers/affix.vue'], resolve)
29 29 },
30 30 {
  31 + path: '/anchor',
  32 + component: (resolve) => require(['./routers/anchor.vue'], resolve)
  33 + },
  34 + {
31 35 path: '/grid',
32 36 component: (resolve) => require(['./routers/grid.vue'], resolve)
33 37 },
... ...
examples/routers/anchor.vue 0 → 100644
  1 +<template>
  2 + <div class="anchor-wrapper">
  3 + <div class="link-wrapper">
  4 + <Button @click="changeCon">修改为Window</Button>
  5 + <Anchor @on-change="handleChange" @on-select="handleSelect" :style="{right: '100px'}" :affix="true" :offset-top="30" :container="scrollCon" show-ink-in-fixed>
  6 + <AnchorLink v-if="(link - 1) % 30 === 0" v-for="link in 300" :key="`link${link}`" :href="`#title-${link}`" :title="`title-${link}`">
  7 + <AnchorLink v-if="link === 61" href="#title-child-69" title="title-child-69"/>
  8 + </AnchorLink>
  9 + </Anchor>
  10 + </div>
  11 + <div v-if="con === 'div'" ref="listWrapper" id="listWrapper" class="list-wrapper">
  12 + <div style="height: 100px;"></div>
  13 + <template v-for="i in 300">
  14 + <h1 v-if="(i - 1) % 30 === 0" :key="`h1${i}`" :id="`title-${i}`">{{ `title-${i}` }}</h1>
  15 + <h1 v-if="i === 69" :key="`h1${i}`" :id="`title-child-${i}`">{{ `title-${i}` }}</h1>
  16 + <h1 v-if="i === 75" :key="`h1${i}`" :id="`title-child-${i}`">{{ `title-${i}` }}</h1>
  17 + <p v-else :key="`p${i}`">{{ `content-row-index-${i}` }}</p>
  18 + <Collapse v-if="i === 3" v-model="value1" :key="`collapse-${i}`">
  19 + <Panel name="1">
  20 + 史蒂夫·乔布斯
  21 + <p v-for="index in 50" :key="`ppp-${index}`" slot="content">{{ index }}</p>
  22 + </Panel>
  23 + <Panel name="2">
  24 + 斯蒂夫·盖瑞·沃兹尼亚克
  25 + <p slot="content">斯蒂夫·盖瑞·沃兹尼亚克(Stephen Gary Wozniak),美国电脑工程师,曾与史蒂夫·乔布斯合伙创立苹果电脑(今之苹果公司)。斯蒂夫·盖瑞·沃兹尼亚克曾就读于美国科罗拉多大学,后转学入美国著名高等学府加州大学伯克利分校(UC Berkeley)并获得电机工程及计算机(EECS)本科学位(1987年)。</p>
  26 + </Panel>
  27 + <Panel name="3">
  28 + 乔纳森·伊夫
  29 + <p slot="content">乔纳森·伊夫是一位工业设计师,现任Apple公司设计师兼资深副总裁,英国爵士。他曾参与设计了iPod,iMac,iPhone,iPad等众多苹果产品。除了乔布斯,他是对苹果那些著名的产品最有影响力的人。</p>
  30 + </Panel>
  31 + </Collapse>
  32 + </template>
  33 + </div>
  34 + <div v-else>
  35 + <template v-for="i in 300">
  36 + <h1 v-if="(i - 1) % 30 === 0" :key="`h1${i}`" :id="`title-${i}`">{{ `title-${i}` }}</h1>
  37 + <h1 v-if="i === 69" :key="`h1${i}`" :id="`title-child-${i}`">{{ `title-${i}` }}</h1>
  38 + <h1 v-if="i === 75" :key="`h1${i}`" :id="`title-child-${i}`">{{ `title-${i}` }}</h1>
  39 + <p v-else :key="`p${i}`">{{ `content-row-index-${i}` }}</p>
  40 + <Collapse v-if="i === 3" v-model="value1" :key="`collapse-${i}`">
  41 + <Panel name="1">
  42 + 史蒂夫·乔布斯
  43 + <p v-for="index in 50" :key="`ppp-${index}`" slot="content">{{ index }}</p>
  44 + </Panel>
  45 + <Panel name="2">
  46 + 斯蒂夫·盖瑞·沃兹尼亚克
  47 + <p slot="content">斯蒂夫·盖瑞·沃兹尼亚克(Stephen Gary Wozniak),美国电脑工程师,曾与史蒂夫·乔布斯合伙创立苹果电脑(今之苹果公司)。斯蒂夫·盖瑞·沃兹尼亚克曾就读于美国科罗拉多大学,后转学入美国著名高等学府加州大学伯克利分校(UC Berkeley)并获得电机工程及计算机(EECS)本科学位(1987年)。</p>
  48 + </Panel>
  49 + <Panel name="3">
  50 + 乔纳森·伊夫
  51 + <p slot="content">乔纳森·伊夫是一位工业设计师,现任Apple公司设计师兼资深副总裁,英国爵士。他曾参与设计了iPod,iMac,iPhone,iPad等众多苹果产品。除了乔布斯,他是对苹果那些著名的产品最有影响力的人。</p>
  52 + </Panel>
  53 + </Collapse>
  54 + </template>
  55 + </div>
  56 +
  57 + </div>
  58 +</template>
  59 +<script>
  60 +export default {
  61 + data () {
  62 + return {
  63 + container: null,
  64 + value1: '1',
  65 + scrollCon: '',
  66 + con: 'div'
  67 + }
  68 + },
  69 + methods: {
  70 + changeCon () {
  71 + this.con = 'window';
  72 + this.scrollCon = undefined;
  73 + },
  74 + handleChange (newHref, oldHref) {
  75 + console.log(`${oldHref} => ${newHref}`)
  76 + },
  77 + handleSelect (href) {
  78 + console.log(`select ${href}`)
  79 + }
  80 + },
  81 + mounted () {
  82 + this.scrollCon = this.$refs.listWrapper
  83 + }
  84 +}
  85 +</script>
  86 +<style lang="less">
  87 +.anchor-wrapper{
  88 + .link-wrapper{
  89 + position: absolute;
  90 + top: 200px;
  91 + right: 100px;
  92 + width: 200px;
  93 + }
  94 + .list-wrapper{
  95 + height: 600px;
  96 + overflow: auto;
  97 + }
  98 +}
  99 +</style>
... ...
src/components/anchor-link/index.js 0 → 100644
  1 +import AnchorLink from '../anchor/anchor-link.vue';
  2 +export default AnchorLink;
... ...
src/components/anchor/anchor-link.vue 0 → 100644
  1 +<template>
  2 + <div :class="anchorLinkClasses">
  3 + <a :class="linkTitleClasses" href="javascript:void(0)" :data-href="href" @click="goAnchor" :title="title">{{ title }}</a>
  4 + <slot></slot>
  5 + </div>
  6 +</template>
  7 +<script>
  8 +import { findComponentUpward } from '../../utils/assist';
  9 +export default {
  10 + name: 'AnchorLink',
  11 + props: {
  12 + href: String,
  13 + title: String
  14 + },
  15 + data () {
  16 + return {
  17 + prefix: 'ivu-anchor-link'
  18 + };
  19 + },
  20 + computed: {
  21 + anchorLinkClasses () {
  22 + return [
  23 + this.prefix,
  24 + this.currentLink === this.href ? `${this.prefix}-active` : ''
  25 + ];
  26 + },
  27 + linkTitleClasses () {
  28 + return [
  29 + `${this.prefix}-title`
  30 + ];
  31 + },
  32 + parentAnchor () {
  33 + return findComponentUpward(this, 'Anchor');
  34 + },
  35 + currentLink () {
  36 + return this.parentAnchor.currentLink;
  37 + }
  38 + },
  39 + methods: {
  40 + goAnchor () {
  41 + this.parentAnchor.turnTo(this.href);
  42 + }
  43 + }
  44 +};
  45 +</script>
... ...
src/components/anchor/anchor.vue 0 → 100644
  1 +<template>
  2 + <component :is="wrapperComponent" :offset-top="offsetTop" :offset-bottom="offsetBottom" @on-change="handleAffixStateChange">
  3 + <div :class="`${prefix}-wrapper`" :style="wrapperStyle">
  4 + <div :class="`${prefix}`">
  5 + <div :class="`${prefix}-ink`">
  6 + <span v-show="showInkBall" :class="`${prefix}-ink-ball`" :style="{top: `${inkTop}px`}"></span>
  7 + </div>
  8 + <slot></slot>
  9 + </div>
  10 + </div>
  11 + </component>
  12 +</template>
  13 +<script>
  14 +import { scrollTop, findComponentDownward, findComponentsDownward, sharpMatcherRegx } from '../../utils/assist';
  15 +import { on, off } from '../../utils/dom';
  16 +export default {
  17 + name: 'Anchor',
  18 + data () {
  19 + return {
  20 + prefix: 'ivu-anchor',
  21 + isAffixed: false, // current affixed state
  22 + inkTop: 0,
  23 + linkHeight: 0,
  24 + animating: false, // if is scrolling now
  25 + currentLink: '', // current show link => #href -> currentLink = #href
  26 + currentId: '', // current show title id => #href -> currentId = href
  27 + scrollContainer: null,
  28 + scrollElement: null,
  29 + titlesOffsetArr: [],
  30 + wrapperTop: 0,
  31 + upperFirstTitle: true
  32 + };
  33 + },
  34 + props: {
  35 + affix: {
  36 + type: Boolean,
  37 + default: true
  38 + },
  39 + offsetTop: {
  40 + type: Number,
  41 + default: 0
  42 + },
  43 + offsetBottom: Number,
  44 + bounds: {
  45 + type: Number,
  46 + default: 5
  47 + },
  48 + container: [String, HTMLElement],
  49 + showInkInFixed: {
  50 + type: Boolean,
  51 + default: false
  52 + }
  53 + },
  54 + computed: {
  55 + wrapperComponent () {
  56 + return this.affix ? 'Affix' : 'div';
  57 + },
  58 + wrapperStyle () {
  59 + return {
  60 + maxHeight: this.offsetTop ? `calc(100vh - ${this.offsetTop}px)` : '100vh'
  61 + };
  62 + },
  63 + containerIsWindow () {
  64 + return this.scrollContainer === window;
  65 + },
  66 + showInkBall () {
  67 + return this.showInkInFixed && (this.isAffixed || (!this.isAffixed && !this.upperFirstTitle && this.scrollContainer !== window));
  68 + }
  69 + },
  70 + methods: {
  71 + handleAffixStateChange (state) {
  72 + this.isAffixed = this.affix && state;
  73 + },
  74 + handleScroll (e) {
  75 + this.upperFirstTitle = e.target.scrollTop < this.titlesOffsetArr[0].offset;
  76 + if (this.animating) return;
  77 + this.updateTitleOffset();
  78 + const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || e.target.scrollTop;
  79 + this.getCurrentScrollAtTitleId(scrollTop);
  80 + },
  81 + turnTo (href) {
  82 + const oldHref = this.currentLink;
  83 + this.currentLink = href;
  84 + this.$router.push({
  85 + path: href
  86 + });
  87 + this.$emit('on-select', href);
  88 + },
  89 + handleHashChange () {
  90 + const url = window.location.href;
  91 + const sharpLinkMatch = sharpMatcherRegx.exec(url);
  92 + this.currentLink = sharpLinkMatch[0];
  93 + this.currentId = sharpLinkMatch[1];
  94 + },
  95 + handleScrollTo () {
  96 + const anchor = document.getElementById(this.currentId);
  97 + if (!anchor) return;
  98 + const offsetTop = anchor.offsetTop - this.wrapperTop;
  99 + this.animating = true;
  100 + scrollTop(this.scrollContainer, this.scrollElement.scrollTop, offsetTop, 600, () => {
  101 + this.animating = false;
  102 + });
  103 + this.handleSetInkTop();
  104 + },
  105 + handleSetInkTop () {
  106 + const currentLinkElementA = document.querySelector(`a[data-href="${this.currentLink}"]`);
  107 + if (!currentLinkElementA) return;
  108 + const elementATop = currentLinkElementA.offsetTop;
  109 + const top = (elementATop < 0 ? this.offsetTop : elementATop);
  110 + this.inkTop = top;
  111 + },
  112 + updateTitleOffset () {
  113 + const links = findComponentsDownward(this, 'AnchorLink').map(link => {
  114 + return link.href;
  115 + });
  116 + const offsetArr = links.map(link => {
  117 + const id = link.split('#')[1];
  118 + const titleEle = document.getElementById(id);
  119 + return {
  120 + link: link,
  121 + offset: titleEle.offsetTop - this.scrollElement.offsetTop
  122 + };
  123 + });
  124 + this.titlesOffsetArr = offsetArr;
  125 + },
  126 + getCurrentScrollAtTitleId (scrollTop) {
  127 + let i = -1;
  128 + let len = this.titlesOffsetArr.length;
  129 + let titleItem = {
  130 + link: '#',
  131 + offset: 0
  132 + };
  133 + scrollTop += this.bounds;
  134 + while (++i < len) {
  135 + let currentEle = this.titlesOffsetArr[i];
  136 + let nextEle = this.titlesOffsetArr[i + 1];
  137 + if (scrollTop >= currentEle.offset && scrollTop < ((nextEle && nextEle.offset) || Infinity)) {
  138 + titleItem = this.titlesOffsetArr[i];
  139 + break;
  140 + }
  141 + }
  142 + this.currentLink = titleItem.link;
  143 + this.handleSetInkTop();
  144 + },
  145 + getContainer () {
  146 + this.scrollContainer = this.container ? (typeof this.container === 'string' ? document.querySelector(this.container) : this.container) : window;
  147 + this.scrollElement = this.container ? this.scrollContainer : (document.documentElement || document.body);
  148 + },
  149 + removeListener () {
  150 + off(this.scrollContainer, 'scroll', this.handleScroll);
  151 + off(window, 'hashchange', this.handleHashChange);
  152 + },
  153 + init () {
  154 + const anchorLink = findComponentDownward(this, 'AnchorLink');
  155 + this.linkHeight = anchorLink ? anchorLink.$el.getBoundingClientRect().height : 0;
  156 + this.handleHashChange();
  157 + this.$nextTick(() => {
  158 + this.removeListener();
  159 + this.getContainer();
  160 + this.wrapperTop = this.containerIsWindow ? 0 : this.scrollElement.offsetTop;
  161 + this.handleScrollTo();
  162 + this.handleSetInkTop();
  163 + this.updateTitleOffset();
  164 + this.upperFirstTitle = this.scrollElement.scrollTop < this.titlesOffsetArr[0].offset;
  165 + on(this.scrollContainer, 'scroll', this.handleScroll);
  166 + on(window, 'hashchange', this.handleHashChange);
  167 + });
  168 + }
  169 + },
  170 + watch: {
  171 + '$route' () {
  172 + this.handleHashChange();
  173 + this.handleScrollTo();
  174 + },
  175 + container () {
  176 + this.init();
  177 + },
  178 + currentLink (newHref, oldHref) {
  179 + this.$emit('on-change', newHref, oldHref);
  180 + }
  181 + },
  182 + mounted () {
  183 + this.init()
  184 + },
  185 + beforeDestroy () {
  186 + this.removeListener();
  187 + }
  188 +};
  189 +</script>
... ...
src/components/anchor/index.js 0 → 100644
  1 +import Anchor from './anchor.vue';
  2 +export default Anchor;
... ...
src/index.js
1 1 import Affix from './components/affix';
2 2 import Alert from './components/alert';
  3 +import Anchor from './components/anchor';
  4 +import AnchorLink from './components/anchor-link';
3 5 import AutoComplete from './components/auto-complete';
4 6 import Avatar from './components/avatar';
5 7 import BackTop from './components/back-top';
... ... @@ -55,6 +57,8 @@ import locale from &#39;./locale/index&#39;;
55 57 const components = {
56 58 Affix,
57 59 Alert,
  60 + Anchor,
  61 + AnchorLink,
58 62 AutoComplete,
59 63 Avatar,
60 64 BackTop,
... ...
src/styles/components/anchor.less 0 → 100644
  1 +@anchor-prefix: ~"@{css-prefix}anchor";
  2 +
  3 +.@{anchor-prefix}{
  4 + &-wrapper{
  5 + background-color: @body-background;
  6 + overflow: auto;
  7 + padding-left: 4px;
  8 + margin-left: -4px;
  9 + }
  10 +
  11 + &{
  12 + position: relative;
  13 + padding-left: @anchor-border-width;
  14 +
  15 + &-ink {
  16 + position: absolute;
  17 + height: 100%;
  18 + left: 0;
  19 + top: 0;
  20 + &:before {
  21 + content: ' ';
  22 + position: relative;
  23 + width: @anchor-border-width;
  24 + height: 100%;
  25 + display: block;
  26 + background-color: @border-color-split;
  27 + margin: 0 auto;
  28 + }
  29 + &-ball {
  30 + display: inline-block;
  31 + position: absolute;
  32 + width: 8px;
  33 + height: 8px;
  34 + border-radius: 8px;
  35 + border: 2px solid @primary-color;
  36 + background-color: @body-background;
  37 + left: 50%;
  38 + transition: top .3s ease-in-out;
  39 + transform: translate(-50%, 2px);
  40 + }
  41 + }
  42 +
  43 + &.fixed &-ink &-ink-ball {
  44 + display: none;
  45 + }
  46 + }
  47 +
  48 + &-link {
  49 + padding: 8px 0 8px 16px;
  50 + line-height: 1;
  51 +
  52 + &-title {
  53 + display: block;
  54 + position: relative;
  55 + transition: all .3s;
  56 + color: @text-color;
  57 + white-space: nowrap;
  58 + overflow: hidden;
  59 + text-overflow: ellipsis;
  60 + margin-bottom: 8px;
  61 + &:only-child {
  62 + margin-bottom: 0;
  63 + }
  64 + }
  65 +
  66 + &-active > &-title {
  67 + color: @primary-color;
  68 + }
  69 + }
  70 +
  71 + &-link &-link {
  72 + padding-top: 6px;
  73 + padding-bottom: 6px;
  74 + }
  75 +}
... ...
src/styles/components/index.less
... ... @@ -44,3 +44,4 @@
44 44 @import "avatar";
45 45 @import "color-picker";
46 46 @import "auto-complete";
  47 +@import "anchor";
... ...
src/styles/custom.less
... ... @@ -184,4 +184,7 @@
184 184 @avatar-font-size-sm: 14px;
185 185 @avatar-bg: #ccc;
186 186 @avatar-color: #fff;
187   -@avatar-border-radius: @border-radius-small;
188 187 \ No newline at end of file
  188 +@avatar-border-radius: @border-radius-small;
  189 +
  190 +// Anchor
  191 +@anchor-border-width: 2px;
... ...
src/utils/assist.js
... ... @@ -138,7 +138,7 @@ function deepCopy(data) {
138 138 export {deepCopy};
139 139  
140 140 // scrollTop animation
141   -export function scrollTop(el, from = 0, to, duration = 500) {
  141 +export function scrollTop(el, from = 0, to, duration = 500, endCallback) {
142 142 if (!window.requestAnimationFrame) {
143 143 window.requestAnimationFrame = (
144 144 window.webkitRequestAnimationFrame ||
... ... @@ -153,7 +153,10 @@ export function scrollTop(el, from = 0, to, duration = 500) {
153 153 const step = Math.ceil(difference / duration * 50);
154 154  
155 155 function scroll(start, end, step) {
156   - if (start === end) return;
  156 + if (start === end) {
  157 + endCallback && endCallback();
  158 + return;
  159 + }
157 160  
158 161 let d = (start + step > end) ? end : start + step;
159 162 if (start > end) {
... ... @@ -322,3 +325,5 @@ export function setMatchMedia () {
322 325 window.matchMedia = window.matchMedia || matchMediaPolyfill;
323 326 }
324 327 }
  328 +
  329 +export const sharpMatcherRegx = /#([^#]+)$/;
... ...
src/utils/dom.js
... ... @@ -33,4 +33,4 @@ export const off = (function() {
33 33 }
34 34 };
35 35 }
36   -})();
37 36 \ No newline at end of file
  37 +})();
... ...