Skip to content

Commit 5c49d1e

Browse files
committed
refactor: move some functions and module-level state into classes as private methods and properties to start to encapsulate Docsify
Also some small tweaks: - move ajax to utils folder - fix some type definitions and improve content in some JSDoc comments - use concise class field syntax - consolidate duplicate docsify-ignore comment removal code - move initGlobalAPI out of Docsify.js to start to encapsulate Docsify This handles a task in [Simplify and modernize Docsify](#2104), as well as works towards [Encapsulating Docsify](#2135).
1 parent b9fe1ce commit 5c49d1e

24 files changed

+663
-637
lines changed

src/core/Docsify.js

+2-7
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ export class Docsify extends Fetch(
1616
// eslint-disable-next-line new-cap
1717
Events(Render(VirtualRoutes(Router(Lifecycle(Object)))))
1818
) {
19+
config = config(this);
20+
1921
constructor() {
2022
super();
2123

22-
this.config = config(this);
23-
2424
this.initLifecycle(); // Init hooks
2525
this.initPlugin(); // Install plugins
2626
this.callHook('init');
@@ -46,8 +46,3 @@ export class Docsify extends Fetch(
4646
});
4747
}
4848
}
49-
50-
/**
51-
* Global API
52-
*/
53-
initGlobalAPI();

src/core/config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { hyphenate, isPrimitive } from './util/core.js';
33

44
const currentScript = document.currentScript;
55

6-
/** @param {import('./Docsify').Docsify} vm */
6+
/** @param {import('./Docsify.js').Docsify} vm */
77
export default function (vm) {
88
const config = Object.assign(
99
{

src/core/event/index.js

+282-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import Tweezer from 'tweezer.js';
12
import { isMobile } from '../util/env.js';
23
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';
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

Comments
 (0)