Commit 1953251a23b82a5ff4302d8cfb54b0f591cd09d2
Committed by
GitHub
Merge pull request #3369 from lison16/anchor
add anchor component
Showing
13 changed files
with
452 additions
and
5 deletions
Show diff stats
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 | }, | ... | ... |
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> | ... | ... |
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> | ... | ... |
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/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 './locale/index'; |
56 | 58 | const components = { |
57 | 59 | Affix, |
58 | 60 | Alert, |
61 | + Anchor, | |
62 | + AnchorLink, | |
59 | 63 | AutoComplete, |
60 | 64 | Avatar, |
61 | 65 | BackTop, | ... | ... |
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
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 = /#([^#]+)$/; | ... | ... |