Commit 1953251a23b82a5ff4302d8cfb54b0f591cd09d2

Authored by Aresn
Committed by GitHub
2 parents 92ffd65d 754eedf5

Merge pull request #3369 from lison16/anchor

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 + <Button @click="andLink">添加一个连接</Button>
  6 + <Anchor @on-change="handleChange" @on-select="handleSelect" :style="{right: '100px'}" :affix="true" :offset-top="30" :container="scrollCon" show-ink-in-fixed>
  7 + <AnchorLink v-if="(link - 1) % 30 === 0" v-for="link in 300" :key="`link${link}`" :href="`#title-${link}`" :title="`title-${link}`">
  8 + <AnchorLink v-if="link === 61" href="#title-child-69" title="title-child-69"/>
  9 + </AnchorLink>
  10 + <AnchorLink v-if="showNewLink" href="#new-link" title="这是动态添加的连接"/>
  11 + </Anchor>
  12 + </div>
  13 + <div v-if="con === 'div'" ref="listWrapper" id="listWrapper" class="list-wrapper">
  14 + <div style="height: 100px;"></div>
  15 + <template v-for="i in 300">
  16 + <h1 v-if="(i - 1) % 30 === 0" :key="`h1${i}`" :id="`title-${i}`">{{ `title-${i}` }}</h1>
  17 + <h1 v-if="i === 69" :key="`h1${i}`" :id="`title-child-${i}`">{{ `title-${i}` }}</h1>
  18 + <h1 v-if="i === 75" :key="`h1${i}`" :id="`title-child-${i}`">{{ `title-${i}` }}</h1>
  19 + <p v-else :key="`p${i}`">{{ `content-row-index-${i}` }}</p>
  20 + <Collapse v-if="i === 3" v-model="value1" :key="`collapse-${i}`">
  21 + <Panel name="1">
  22 + 史蒂夫·乔布斯
  23 + <p v-for="index in 50" :key="`ppp-${index}`" slot="content">{{ index }}</p>
  24 + </Panel>
  25 + <Panel name="2">
  26 + 斯蒂夫·盖瑞·沃兹尼亚克
  27 + <p slot="content">斯蒂夫·盖瑞·沃兹尼亚克(Stephen Gary Wozniak),美国电脑工程师,曾与史蒂夫·乔布斯合伙创立苹果电脑(今之苹果公司)。斯蒂夫·盖瑞·沃兹尼亚克曾就读于美国科罗拉多大学,后转学入美国著名高等学府加州大学伯克利分校(UC Berkeley)并获得电机工程及计算机(EECS)本科学位(1987年)。</p>
  28 + </Panel>
  29 + <Panel name="3">
  30 + 乔纳森·伊夫
  31 + <p slot="content">乔纳森·伊夫是一位工业设计师,现任Apple公司设计师兼资深副总裁,英国爵士。他曾参与设计了iPod,iMac,iPhone,iPad等众多苹果产品。除了乔布斯,他是对苹果那些著名的产品最有影响力的人。</p>
  32 + </Panel>
  33 + </Collapse>
  34 + </template>
  35 + <!-- <h1 id="new-link">这是新添加的哦哦哦哦哦 哦 </h1>
  36 + <p v-for="i in 50" :key="`new-${i}`">这是信息司大是大非胜多负少的{{i}}</p> -->
  37 + </div>
  38 + <div v-else>
  39 + <template v-for="i in 300">
  40 + <h1 v-if="(i - 1) % 30 === 0" :key="`h1${i}`" :id="`title-${i}`">{{ `title-${i}` }}</h1>
  41 + <h1 v-if="i === 69" :key="`h1${i}`" :id="`title-child-${i}`">{{ `title-${i}` }}</h1>
  42 + <h1 v-if="i === 75" :key="`h1${i}`" :id="`title-child-${i}`">{{ `title-${i}` }}</h1>
  43 + <p v-else :key="`p${i}`">{{ `content-row-index-${i}` }}</p>
  44 + <Collapse v-if="i === 3" v-model="value1" :key="`collapse-${i}`">
  45 + <Panel name="1">
  46 + 史蒂夫·乔布斯
  47 + <p v-for="index in 50" :key="`ppp-${index}`" slot="content">{{ index }}</p>
  48 + </Panel>
  49 + <Panel name="2">
  50 + 斯蒂夫·盖瑞·沃兹尼亚克
  51 + <p slot="content">斯蒂夫·盖瑞·沃兹尼亚克(Stephen Gary Wozniak),美国电脑工程师,曾与史蒂夫·乔布斯合伙创立苹果电脑(今之苹果公司)。斯蒂夫·盖瑞·沃兹尼亚克曾就读于美国科罗拉多大学,后转学入美国著名高等学府加州大学伯克利分校(UC Berkeley)并获得电机工程及计算机(EECS)本科学位(1987年)。</p>
  52 + </Panel>
  53 + <Panel name="3">
  54 + 乔纳森·伊夫
  55 + <p slot="content">乔纳森·伊夫是一位工业设计师,现任Apple公司设计师兼资深副总裁,英国爵士。他曾参与设计了iPod,iMac,iPhone,iPad等众多苹果产品。除了乔布斯,他是对苹果那些著名的产品最有影响力的人。</p>
  56 + </Panel>
  57 + </Collapse>
  58 + </template>
  59 + <h1 id="new-link">这是新添加的哦哦哦哦哦 哦 </h1>
  60 + <p v-for="i in 50" :key="`new-${i}`">这是信息司大是大非胜多负少的{{i}}</p>
  61 + </div>
  62 +
  63 + </div>
  64 +</template>
  65 +<script>
  66 +export default {
  67 + data () {
  68 + return {
  69 + container: null,
  70 + value1: '1',
  71 + scrollCon: '',
  72 + con: 'div',
  73 + showNewLink: false
  74 + }
  75 + },
  76 + methods: {
  77 + changeCon () {
  78 + this.con = 'window';
  79 + this.scrollCon = undefined;
  80 + },
  81 + handleChange (newHref, oldHref) {
  82 + console.log(`${oldHref} => ${newHref}`)
  83 + },
  84 + handleSelect (href) {
  85 + console.log(`select ${href}`)
  86 + },
  87 + andLink () {
  88 + this.showNewLink = true;
  89 + }
  90 + },
  91 + mounted () {
  92 + this.scrollCon = this.$refs.listWrapper
  93 + }
  94 +}
  95 +</script>
  96 +<style lang="less">
  97 +.anchor-wrapper{
  98 + .link-wrapper{
  99 + position: absolute;
  100 + top: 200px;
  101 + right: 100px;
  102 + width: 200px;
  103 + }
  104 + .list-wrapper{
  105 + height: 600px;
  106 + overflow: auto;
  107 + }
  108 +}
  109 +</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 + mounted () {
  45 + this.$nextTick(() => {
  46 + this.parentAnchor.init();
  47 + });
  48 + }
  49 +};
  50 +</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 + this.currentLink = href;
  83 + this.$router.push({
  84 + path: href
  85 + });
  86 + this.$emit('on-select', href);
  87 + },
  88 + handleHashChange () {
  89 + const url = window.location.href;
  90 + const sharpLinkMatch = sharpMatcherRegx.exec(url);
  91 + this.currentLink = sharpLinkMatch[0];
  92 + this.currentId = sharpLinkMatch[1];
  93 + },
  94 + handleScrollTo () {
  95 + const anchor = document.getElementById(this.currentId);
  96 + if (!anchor) return;
  97 + const offsetTop = anchor.offsetTop - this.wrapperTop;
  98 + this.animating = true;
  99 + scrollTop(this.scrollContainer, this.scrollElement.scrollTop, offsetTop, 600, () => {
  100 + this.animating = false;
  101 + });
  102 + this.handleSetInkTop();
  103 + },
  104 + handleSetInkTop () {
  105 + const currentLinkElementA = document.querySelector(`a[data-href="${this.currentLink}"]`);
  106 + if (!currentLinkElementA) return;
  107 + const elementATop = currentLinkElementA.offsetTop;
  108 + const top = (elementATop < 0 ? this.offsetTop : elementATop);
  109 + this.inkTop = top;
  110 + },
  111 + updateTitleOffset () {
  112 + const links = findComponentsDownward(this, 'AnchorLink').map(link => {
  113 + return link.href;
  114 + });
  115 + const idArr = links.map(link => {
  116 + return link.split('#')[1];
  117 + });
  118 + let offsetArr = [];
  119 + idArr.forEach(id => {
  120 + const titleEle = document.getElementById(id);
  121 + if (titleEle) offsetArr.push({
  122 + link: `#${id}`,
  123 + offset: titleEle.offsetTop - this.scrollElement.offsetTop
  124 + });
  125 + });
  126 + this.titlesOffsetArr = offsetArr;
  127 + },
  128 + getCurrentScrollAtTitleId (scrollTop) {
  129 + let i = -1;
  130 + let len = this.titlesOffsetArr.length;
  131 + let titleItem = {
  132 + link: '#',
  133 + offset: 0
  134 + };
  135 + scrollTop += this.bounds;
  136 + while (++i < len) {
  137 + let currentEle = this.titlesOffsetArr[i];
  138 + let nextEle = this.titlesOffsetArr[i + 1];
  139 + if (scrollTop >= currentEle.offset && scrollTop < ((nextEle && nextEle.offset) || Infinity)) {
  140 + titleItem = this.titlesOffsetArr[i];
  141 + break;
  142 + }
  143 + }
  144 + this.currentLink = titleItem.link;
  145 + this.handleSetInkTop();
  146 + },
  147 + getContainer () {
  148 + this.scrollContainer = this.container ? (typeof this.container === 'string' ? document.querySelector(this.container) : this.container) : window;
  149 + this.scrollElement = this.container ? this.scrollContainer : (document.documentElement || document.body);
  150 + },
  151 + removeListener () {
  152 + off(this.scrollContainer, 'scroll', this.handleScroll);
  153 + off(window, 'hashchange', this.handleHashChange);
  154 + },
  155 + init () {
  156 + const anchorLink = findComponentDownward(this, 'AnchorLink');
  157 + this.linkHeight = anchorLink ? anchorLink.$el.getBoundingClientRect().height : 0;
  158 + this.handleHashChange();
  159 + this.$nextTick(() => {
  160 + this.removeListener();
  161 + this.getContainer();
  162 + this.wrapperTop = this.containerIsWindow ? 0 : this.scrollElement.offsetTop;
  163 + this.handleScrollTo();
  164 + this.handleSetInkTop();
  165 + this.updateTitleOffset();
  166 + this.upperFirstTitle = this.scrollElement.scrollTop < this.titlesOffsetArr[0].offset;
  167 + on(this.scrollContainer, 'scroll', this.handleScroll);
  168 + on(window, 'hashchange', this.handleHashChange);
  169 + });
  170 + }
  171 + },
  172 + watch: {
  173 + '$route' () {
  174 + this.handleHashChange();
  175 + this.handleScrollTo();
  176 + },
  177 + container () {
  178 + this.init();
  179 + },
  180 + currentLink (newHref, oldHref) {
  181 + this.$emit('on-change', newHref, oldHref);
  182 + }
  183 + },
  184 + mounted () {
  185 + this.init();
  186 + },
  187 + beforeDestroy () {
  188 + this.removeListener();
  189 + }
  190 +};
  191 +</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';
... ... @@ -56,6 +58,8 @@ import locale from &#39;./locale/index&#39;;
56 58 const components = {
57 59 Affix,
58 60 Alert,
  61 + Anchor,
  62 + AnchorLink,
59 63 AutoComplete,
60 64 Avatar,
61 65 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,4 +44,5 @@
44 44 @import "avatar";
45 45 @import "color-picker";
46 46 @import "auto-complete";
47   -@import "time";
48 47 \ No newline at end of file
  48 +@import "anchor";
  49 +@import "time";
... ...
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 +})();
... ...