1+ import Tweezer from 'tweezer.js' ;
12import { isMobile } from '../util/env.js' ;
23import { 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' ;
57
6- /** @typedef {import('../Docsify').Constructor } Constructor */
8+ /** @typedef {import('../Docsify.js ').Constructor } Constructor */
79
810/**
911 * @template {!Constructor} T
@@ -18,29 +20,300 @@ export function Events(Base) {
1820 if ( source !== 'history' ) {
1921 // Scroll to ID if specified
2022 if ( this . route . query . id ) {
21- scrollIntoView ( this . route . path , this . route . query . id ) ;
23+ this . # scrollIntoView( this . route . path , this . route . query . id ) ;
2224 }
2325 // Scroll to top if a link was clicked and auto2top is enabled
2426 if ( source === 'navigate' ) {
25- auto2top && scroll2Top ( auto2top ) ;
27+ auto2top && this . # scroll2Top( auto2top ) ;
2628 }
2729 }
2830
2931 if ( this . config . loadNavbar ) {
30- sidebar . getAndActive ( this . router , 'nav' ) ;
32+ this . __getAndActive ( this . router , 'nav' ) ;
3133 }
3234 }
3335
3436 initEvent ( ) {
3537 // 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 ) ;
3840 // Bind sticky effect
3941 if ( this . config . coverpage ) {
40- ! isMobile && on ( 'scroll' , sidebar . sticky ) ;
42+ ! isMobile && on ( 'scroll' , this . __sticky ) ;
4143 } else {
4244 body . classList . add ( 'sticky' ) ;
4345 }
4446 }
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+ }
45318 } ;
46319}
0 commit comments