1
+ import Tweezer from 'tweezer.js' ;
1
2
import { isMobile } from '../util/env.js' ;
2
3
import { body , on } from '../util/dom.js' ;
3
- import * as sidebar from './sidebar.js' ;
4
- import { scrollIntoView , scroll2Top } from './scroll.js' ;
4
+ import * as dom from '../util/dom.js' ;
5
+ import { removeParams } from '../router/util.js' ;
6
+ import config from '../config.js' ;
5
7
6
- /** @typedef {import('../Docsify').Constructor } Constructor */
8
+ /** @typedef {import('../Docsify.js ').Constructor } Constructor */
7
9
8
10
/**
9
11
* @template {!Constructor} T
@@ -18,29 +20,300 @@ export function Events(Base) {
18
20
if ( source !== 'history' ) {
19
21
// Scroll to ID if specified
20
22
if ( this . route . query . id ) {
21
- scrollIntoView ( this . route . path , this . route . query . id ) ;
23
+ this . # scrollIntoView( this . route . path , this . route . query . id ) ;
22
24
}
23
25
// Scroll to top if a link was clicked and auto2top is enabled
24
26
if ( source === 'navigate' ) {
25
- auto2top && scroll2Top ( auto2top ) ;
27
+ auto2top && this . # scroll2Top( auto2top ) ;
26
28
}
27
29
}
28
30
29
31
if ( this . config . loadNavbar ) {
30
- sidebar . getAndActive ( this . router , 'nav' ) ;
32
+ this . __getAndActive ( this . router , 'nav' ) ;
31
33
}
32
34
}
33
35
34
36
initEvent ( ) {
35
37
// Bind toggle button
36
- sidebar . btn ( 'button.sidebar-toggle' , this . router ) ;
37
- sidebar . collapse ( '.sidebar' , this . router ) ;
38
+ this . # btn( 'button.sidebar-toggle' , this . router ) ;
39
+ this . # collapse( '.sidebar' , this . router ) ;
38
40
// Bind sticky effect
39
41
if ( this . config . coverpage ) {
40
- ! isMobile && on ( 'scroll' , sidebar . sticky ) ;
42
+ ! isMobile && on ( 'scroll' , this . __sticky ) ;
41
43
} else {
42
44
body . classList . add ( 'sticky' ) ;
43
45
}
44
46
}
47
+
48
+ /** @readonly */
49
+ #nav = { } ;
50
+
51
+ #hoverOver = false ;
52
+ #scroller = null ;
53
+ #enableScrollEvent = true ;
54
+ #coverHeight = 0 ;
55
+
56
+ #scrollTo( el , offset = 0 ) {
57
+ if ( this . #scroller) {
58
+ this . #scroller. stop ( ) ;
59
+ }
60
+
61
+ this . #enableScrollEvent = false ;
62
+ this . #scroller = new Tweezer ( {
63
+ start : window . pageYOffset ,
64
+ end :
65
+ Math . round ( el . getBoundingClientRect ( ) . top ) +
66
+ window . pageYOffset -
67
+ offset ,
68
+ duration : 500 ,
69
+ } )
70
+ . on ( 'tick' , v => window . scrollTo ( 0 , v ) )
71
+ . on ( 'done' , ( ) => {
72
+ this . #enableScrollEvent = true ;
73
+ this . #scroller = null ;
74
+ } )
75
+ . begin ( ) ;
76
+ }
77
+
78
+ #highlight( path ) {
79
+ if ( ! this . #enableScrollEvent) {
80
+ return ;
81
+ }
82
+
83
+ const sidebar = dom . getNode ( '.sidebar' ) ;
84
+ const anchors = dom . findAll ( '.anchor' ) ;
85
+ const wrap = dom . find ( sidebar , '.sidebar-nav' ) ;
86
+ let active = dom . find ( sidebar , 'li.active' ) ;
87
+ const doc = document . documentElement ;
88
+ const top =
89
+ ( ( doc && doc . scrollTop ) || document . body . scrollTop ) - this . #coverHeight;
90
+ let last ;
91
+
92
+ for ( const node of anchors ) {
93
+ if ( node . offsetTop > top ) {
94
+ if ( ! last ) {
95
+ last = node ;
96
+ }
97
+
98
+ break ;
99
+ } else {
100
+ last = node ;
101
+ }
102
+ }
103
+
104
+ if ( ! last ) {
105
+ return ;
106
+ }
107
+
108
+ const li = this . #nav[ this . #getNavKey( path , last . getAttribute ( 'data-id' ) ) ] ;
109
+
110
+ if ( ! li || li === active ) {
111
+ return ;
112
+ }
113
+
114
+ active && active . classList . remove ( 'active' ) ;
115
+ li . classList . add ( 'active' ) ;
116
+ active = li ;
117
+
118
+ // Scroll into view
119
+ // https://github.com/vuejs/vuejs.org/blob/master/themes/vue/source/js/common.js#L282-L297
120
+ if ( ! this . #hoverOver && dom . body . classList . contains ( 'sticky' ) ) {
121
+ const height = sidebar . clientHeight ;
122
+ const curOffset = 0 ;
123
+ const cur = active . offsetTop + active . clientHeight + 40 ;
124
+ const isInView =
125
+ active . offsetTop >= wrap . scrollTop && cur <= wrap . scrollTop + height ;
126
+ const notThan = cur - curOffset < height ;
127
+
128
+ sidebar . scrollTop = isInView
129
+ ? wrap . scrollTop
130
+ : notThan
131
+ ? curOffset
132
+ : cur - height ;
133
+ }
134
+ }
135
+
136
+ #getNavKey( path , id ) {
137
+ return `${ decodeURIComponent ( path ) } ?id=${ decodeURIComponent ( id ) } ` ;
138
+ }
139
+
140
+ __scrollActiveSidebar ( router ) {
141
+ const cover = dom . find ( '.cover.show' ) ;
142
+ this . #coverHeight = cover ? cover . offsetHeight : 0 ;
143
+
144
+ const sidebar = dom . getNode ( '.sidebar' ) ;
145
+ let lis = [ ] ;
146
+ if ( sidebar !== null && sidebar !== undefined ) {
147
+ lis = dom . findAll ( sidebar , 'li' ) ;
148
+ }
149
+
150
+ for ( const li of lis ) {
151
+ const a = li . querySelector ( 'a' ) ;
152
+ if ( ! a ) {
153
+ continue ;
154
+ }
155
+
156
+ let href = a . getAttribute ( 'href' ) ;
157
+
158
+ if ( href !== '/' ) {
159
+ const {
160
+ query : { id } ,
161
+ path,
162
+ } = router . parse ( href ) ;
163
+ if ( id ) {
164
+ href = this . #getNavKey( path , id ) ;
165
+ }
166
+ }
167
+
168
+ if ( href ) {
169
+ this . #nav[ decodeURIComponent ( href ) ] = li ;
170
+ }
171
+ }
172
+
173
+ if ( isMobile ) {
174
+ return ;
175
+ }
176
+
177
+ const path = removeParams ( router . getCurrentPath ( ) ) ;
178
+ dom . off ( 'scroll' , ( ) => this . #highlight( path ) ) ;
179
+ dom . on ( 'scroll' , ( ) => this . #highlight( path ) ) ;
180
+ dom . on ( sidebar , 'mouseover' , ( ) => {
181
+ this . #hoverOver = true ;
182
+ } ) ;
183
+ dom . on ( sidebar , 'mouseleave' , ( ) => {
184
+ this . #hoverOver = false ;
185
+ } ) ;
186
+ }
187
+
188
+ #scrollIntoView( path , id ) {
189
+ if ( ! id ) {
190
+ return ;
191
+ }
192
+ const topMargin = config ( ) . topMargin ;
193
+ // Use [id='1234'] instead of #id to handle special cases such as reserved characters and pure number id
194
+ // https://stackoverflow.com/questions/37270787/uncaught-syntaxerror-failed-to-execute-queryselector-on-document
195
+ const section = dom . find ( "[id='" + id + "']" ) ;
196
+ section && this . #scrollTo( section , topMargin ) ;
197
+
198
+ const li = this . #nav[ this . #getNavKey( path , id ) ] ;
199
+ const sidebar = dom . getNode ( '.sidebar' ) ;
200
+ const active = dom . find ( sidebar , 'li.active' ) ;
201
+ active && active . classList . remove ( 'active' ) ;
202
+ li && li . classList . add ( 'active' ) ;
203
+ }
204
+
205
+ #scrollEl = dom . $ . scrollingElement || dom . $ . documentElement ;
206
+
207
+ #scroll2Top( offset = 0 ) {
208
+ this . #scrollEl. scrollTop = offset === true ? 0 : Number ( offset ) ;
209
+ }
210
+
211
+ /** @readonly */
212
+ #title = dom . $ . title ;
213
+
214
+ /**
215
+ * Toggle button
216
+ * @param {Element } el Button to be toggled
217
+ * @void
218
+ */
219
+ #btn( el ) {
220
+ const toggle = _ => dom . body . classList . toggle ( 'close' ) ;
221
+
222
+ el = dom . getNode ( el ) ;
223
+ if ( el === null || el === undefined ) {
224
+ return ;
225
+ }
226
+
227
+ dom . on ( el , 'click' , e => {
228
+ e . stopPropagation ( ) ;
229
+ toggle ( ) ;
230
+ } ) ;
231
+
232
+ isMobile &&
233
+ dom . on (
234
+ dom . body ,
235
+ 'click' ,
236
+ _ => dom . body . classList . contains ( 'close' ) && toggle ( )
237
+ ) ;
238
+ }
239
+
240
+ #collapse( el ) {
241
+ el = dom . getNode ( el ) ;
242
+ if ( el === null || el === undefined ) {
243
+ return ;
244
+ }
245
+
246
+ dom . on ( el , 'click' , ( { target } ) => {
247
+ if (
248
+ target . nodeName === 'A' &&
249
+ target . nextSibling &&
250
+ target . nextSibling . classList &&
251
+ target . nextSibling . classList . contains ( 'app-sub-sidebar' )
252
+ ) {
253
+ dom . toggleClass ( target . parentNode , 'collapse' ) ;
254
+ }
255
+ } ) ;
256
+ }
257
+
258
+ __sticky = ( ) => {
259
+ const cover = dom . getNode ( 'section.cover' ) ;
260
+ if ( ! cover ) {
261
+ return ;
262
+ }
263
+
264
+ const coverHeight = cover . getBoundingClientRect ( ) . height ;
265
+
266
+ if (
267
+ window . pageYOffset >= coverHeight ||
268
+ cover . classList . contains ( 'hidden' )
269
+ ) {
270
+ dom . toggleClass ( dom . body , 'add' , 'sticky' ) ;
271
+ } else {
272
+ dom . toggleClass ( dom . body , 'remove' , 'sticky' ) ;
273
+ }
274
+ } ;
275
+
276
+ /**
277
+ * Get and active link
278
+ * @param {Object } router Router
279
+ * @param {String|Element } el Target element
280
+ * @param {Boolean } isParent Active parent
281
+ * @param {Boolean } autoTitle Automatically set title
282
+ * @return {Element } Active element
283
+ */
284
+ __getAndActive ( router , el , isParent , autoTitle ) {
285
+ el = dom . getNode ( el ) ;
286
+ let links = [ ] ;
287
+ if ( el !== null && el !== undefined ) {
288
+ links = dom . findAll ( el , 'a' ) ;
289
+ }
290
+
291
+ const hash = decodeURI ( router . toURL ( router . getCurrentPath ( ) ) ) ;
292
+ let target ;
293
+
294
+ links
295
+ . sort ( ( a , b ) => b . href . length - a . href . length )
296
+ . forEach ( a => {
297
+ const href = decodeURI ( a . getAttribute ( 'href' ) ) ;
298
+ const node = isParent ? a . parentNode : a ;
299
+
300
+ a . title = a . title || a . innerText ;
301
+
302
+ if ( hash . indexOf ( href ) === 0 && ! target ) {
303
+ target = a ;
304
+ dom . toggleClass ( node , 'add' , 'active' ) ;
305
+ } else {
306
+ dom . toggleClass ( node , 'remove' , 'active' ) ;
307
+ }
308
+ } ) ;
309
+
310
+ if ( autoTitle ) {
311
+ dom . $ . title = target
312
+ ? target . title || `${ target . innerText } - ${ this . #title} `
313
+ : this . #title;
314
+ }
315
+
316
+ return target ;
317
+ }
45
318
} ;
46
319
}
0 commit comments