Skip to content

Commit

Permalink
refactor: move some functions and module-level state into classes as …
Browse files Browse the repository at this point in the history
…private methods and properties to start to encapsulate Docsify

Also some small tweaks:

- move initGlobalAPI out of Docsify.js to start to encapsulate Docsify
- 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

This handles a task in [Simplify and modernize Docsify](#2104), as well as works towards [Encapsulating Docsify](#2135).
  • Loading branch information
trusktr committed Jul 17, 2023
1 parent b9fe1ce commit 9f8c3c9
Show file tree
Hide file tree
Showing 24 changed files with 663 additions and 638 deletions.
10 changes: 2 additions & 8 deletions src/core/Docsify.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Render } from './render/index.js';
import { Fetch } from './fetch/index.js';
import { Events } from './event/index.js';
import { VirtualRoutes } from './virtual-routes/index.js';
import initGlobalAPI from './global-api.js';

import config from './config.js';
import { isFn } from './util/core.js';
Expand All @@ -16,11 +15,11 @@ export class Docsify extends Fetch(
// eslint-disable-next-line new-cap
Events(Render(VirtualRoutes(Router(Lifecycle(Object)))))
) {
config = config(this);

constructor() {
super();

this.config = config(this);

this.initLifecycle(); // Init hooks
this.initPlugin(); // Install plugins
this.callHook('init');
Expand All @@ -46,8 +45,3 @@ export class Docsify extends Fetch(
});
}
}

/**
* Global API
*/
initGlobalAPI();
2 changes: 1 addition & 1 deletion src/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { hyphenate, isPrimitive } from './util/core.js';

const currentScript = document.currentScript;

/** @param {import('./Docsify').Docsify} vm */
/** @param {import('./Docsify.js').Docsify} vm */
export default function (vm) {
const config = Object.assign(
{
Expand Down
291 changes: 282 additions & 9 deletions src/core/event/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Tweezer from 'tweezer.js';
import { isMobile } from '../util/env.js';
import { body, on } from '../util/dom.js';
import * as sidebar from './sidebar.js';
import { scrollIntoView, scroll2Top } from './scroll.js';
import * as dom from '../util/dom.js';
import { removeParams } from '../router/util.js';
import config from '../config.js';

/** @typedef {import('../Docsify').Constructor} Constructor */
/** @typedef {import('../Docsify.js').Constructor} Constructor */

/**
* @template {!Constructor} T
Expand All @@ -18,29 +20,300 @@ export function Events(Base) {
if (source !== 'history') {
// Scroll to ID if specified
if (this.route.query.id) {
scrollIntoView(this.route.path, this.route.query.id);
this.#scrollIntoView(this.route.path, this.route.query.id);
}
// Scroll to top if a link was clicked and auto2top is enabled
if (source === 'navigate') {
auto2top && scroll2Top(auto2top);
auto2top && this.#scroll2Top(auto2top);
}
}

if (this.config.loadNavbar) {
sidebar.getAndActive(this.router, 'nav');
this.__getAndActive(this.router, 'nav');
}
}

initEvent() {
// Bind toggle button
sidebar.btn('button.sidebar-toggle', this.router);
sidebar.collapse('.sidebar', this.router);
this.#btn('button.sidebar-toggle', this.router);
this.#collapse('.sidebar', this.router);
// Bind sticky effect
if (this.config.coverpage) {
!isMobile && on('scroll', sidebar.sticky);
!isMobile && on('scroll', this.__sticky);
} else {
body.classList.add('sticky');
}
}

/** @readonly */
#nav = {};

#hoverOver = false;
#scroller = null;
#enableScrollEvent = true;
#coverHeight = 0;

#scrollTo(el, offset = 0) {
if (this.#scroller) {
this.#scroller.stop();
}

this.#enableScrollEvent = false;
this.#scroller = new Tweezer({
start: window.pageYOffset,
end:
Math.round(el.getBoundingClientRect().top) +
window.pageYOffset -
offset,
duration: 500,
})
.on('tick', v => window.scrollTo(0, v))
.on('done', () => {
this.#enableScrollEvent = true;
this.#scroller = null;
})
.begin();
}

#highlight(path) {
if (!this.#enableScrollEvent) {
return;
}

const sidebar = dom.getNode('.sidebar');
const anchors = dom.findAll('.anchor');
const wrap = dom.find(sidebar, '.sidebar-nav');
let active = dom.find(sidebar, 'li.active');
const doc = document.documentElement;
const top =
((doc && doc.scrollTop) || document.body.scrollTop) - this.#coverHeight;
let last;

for (const node of anchors) {
if (node.offsetTop > top) {
if (!last) {
last = node;
}

break;
} else {
last = node;
}
}

if (!last) {
return;
}

const li = this.#nav[this.#getNavKey(path, last.getAttribute('data-id'))];

if (!li || li === active) {
return;
}

active && active.classList.remove('active');
li.classList.add('active');
active = li;

// Scroll into view
// https://github.com/vuejs/vuejs.org/blob/master/themes/vue/source/js/common.js#L282-L297
if (!this.#hoverOver && dom.body.classList.contains('sticky')) {
const height = sidebar.clientHeight;
const curOffset = 0;
const cur = active.offsetTop + active.clientHeight + 40;
const isInView =
active.offsetTop >= wrap.scrollTop && cur <= wrap.scrollTop + height;
const notThan = cur - curOffset < height;

sidebar.scrollTop = isInView
? wrap.scrollTop
: notThan
? curOffset
: cur - height;
}
}

#getNavKey(path, id) {
return `${decodeURIComponent(path)}?id=${decodeURIComponent(id)}`;
}

__scrollActiveSidebar(router) {
const cover = dom.find('.cover.show');
this.#coverHeight = cover ? cover.offsetHeight : 0;

const sidebar = dom.getNode('.sidebar');
let lis = [];
if (sidebar !== null && sidebar !== undefined) {
lis = dom.findAll(sidebar, 'li');
}

for (const li of lis) {
const a = li.querySelector('a');
if (!a) {
continue;
}

let href = a.getAttribute('href');

if (href !== '/') {
const {
query: { id },
path,
} = router.parse(href);
if (id) {
href = this.#getNavKey(path, id);
}
}

if (href) {
this.#nav[decodeURIComponent(href)] = li;
}
}

if (isMobile) {
return;
}

const path = removeParams(router.getCurrentPath());
dom.off('scroll', () => this.#highlight(path));
dom.on('scroll', () => this.#highlight(path));
dom.on(sidebar, 'mouseover', () => {
this.#hoverOver = true;
});
dom.on(sidebar, 'mouseleave', () => {
this.#hoverOver = false;
});
}

#scrollIntoView(path, id) {
if (!id) {
return;
}
const topMargin = config().topMargin;
// Use [id='1234'] instead of #id to handle special cases such as reserved characters and pure number id
// https://stackoverflow.com/questions/37270787/uncaught-syntaxerror-failed-to-execute-queryselector-on-document
const section = dom.find("[id='" + id + "']");
section && this.#scrollTo(section, topMargin);

const li = this.#nav[this.#getNavKey(path, id)];
const sidebar = dom.getNode('.sidebar');
const active = dom.find(sidebar, 'li.active');
active && active.classList.remove('active');
li && li.classList.add('active');
}

#scrollEl = dom.$.scrollingElement || dom.$.documentElement;

#scroll2Top(offset = 0) {
this.#scrollEl.scrollTop = offset === true ? 0 : Number(offset);
}

/** @readonly */
#title = dom.$.title;

/**
* Toggle button
* @param {Element} el Button to be toggled
* @void
*/
#btn(el) {
const toggle = _ => dom.body.classList.toggle('close');

el = dom.getNode(el);
if (el === null || el === undefined) {
return;
}

dom.on(el, 'click', e => {
e.stopPropagation();
toggle();
});

isMobile &&
dom.on(
dom.body,
'click',
_ => dom.body.classList.contains('close') && toggle()
);
}

#collapse(el) {
el = dom.getNode(el);
if (el === null || el === undefined) {
return;
}

dom.on(el, 'click', ({ target }) => {
if (
target.nodeName === 'A' &&
target.nextSibling &&
target.nextSibling.classList &&
target.nextSibling.classList.contains('app-sub-sidebar')
) {
dom.toggleClass(target.parentNode, 'collapse');
}
});
}

__sticky = () => {
const cover = dom.getNode('section.cover');
if (!cover) {
return;
}

const coverHeight = cover.getBoundingClientRect().height;

if (
window.pageYOffset >= coverHeight ||
cover.classList.contains('hidden')
) {
dom.toggleClass(dom.body, 'add', 'sticky');
} else {
dom.toggleClass(dom.body, 'remove', 'sticky');
}
};

/**
* Get and active link
* @param {Object} router Router
* @param {String|Element} el Target element
* @param {Boolean} isParent Active parent
* @param {Boolean} autoTitle Automatically set title
* @return {Element} Active element
*/
__getAndActive(router, el, isParent, autoTitle) {
el = dom.getNode(el);
let links = [];
if (el !== null && el !== undefined) {
links = dom.findAll(el, 'a');
}

const hash = decodeURI(router.toURL(router.getCurrentPath()));
let target;

links
.sort((a, b) => b.href.length - a.href.length)
.forEach(a => {
const href = decodeURI(a.getAttribute('href'));
const node = isParent ? a.parentNode : a;

a.title = a.title || a.innerText;

if (hash.indexOf(href) === 0 && !target) {
target = a;
dom.toggleClass(node, 'add', 'active');
} else {
dom.toggleClass(node, 'remove', 'active');
}
});

if (autoTitle) {
dom.$.title = target
? target.title || `${target.innerText} - ${this.#title}`
: this.#title;
}

return target;
}
};
}

0 comments on commit 9f8c3c9

Please sign in to comment.