mirror of
https://github.com/docsifyjs/docsify.git
synced 2025-12-08 19:55:52 +00:00
Merge pull request #1685 from docsifyjs/classes
convert Docsify and mixins to ES classes
This commit is contained in:
commit
40a5fa8f44
@ -1,22 +1,39 @@
|
||||
import { initMixin } from './init';
|
||||
import { routerMixin } from './router';
|
||||
import { renderMixin } from './render';
|
||||
import { fetchMixin } from './fetch';
|
||||
import { eventMixin } from './event';
|
||||
import initGlobalAPI from './global-api';
|
||||
import { Router } from './router/index.js';
|
||||
import { Render } from './render/index.js';
|
||||
import { Fetch } from './fetch/index.js';
|
||||
import { Events } from './event/index.js';
|
||||
import initGlobalAPI from './global-api.js';
|
||||
|
||||
export function Docsify() {
|
||||
this._init();
|
||||
import config from './config.js';
|
||||
import { isFn } from './util/core';
|
||||
import { Lifecycle } from './init/lifecycle';
|
||||
|
||||
/** @typedef {new (...args: any[]) => any} Constructor */
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
export class Docsify extends Fetch(Events(Render(Router(Lifecycle(Object))))) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.config = config(this);
|
||||
|
||||
this.initLifecycle(); // Init hooks
|
||||
this.initPlugin(); // Install plugins
|
||||
this.callHook('init');
|
||||
this.initRouter(); // Add router
|
||||
this.initRender(); // Render base DOM
|
||||
this.initEvent(); // Bind events
|
||||
this.initFetch(); // Fetch data
|
||||
this.callHook('mounted');
|
||||
}
|
||||
|
||||
initPlugin() {
|
||||
[]
|
||||
.concat(this.config.plugins)
|
||||
.forEach(fn => isFn(fn) && fn(this._lifecycle, this));
|
||||
}
|
||||
}
|
||||
|
||||
const proto = Docsify.prototype;
|
||||
|
||||
initMixin(proto);
|
||||
routerMixin(proto);
|
||||
renderMixin(proto);
|
||||
fetchMixin(proto);
|
||||
eventMixin(proto);
|
||||
|
||||
/**
|
||||
* Global API
|
||||
*/
|
||||
|
||||
@ -2,6 +2,7 @@ import { merge, hyphenate, isPrimitive, hasOwn } from './util/core';
|
||||
|
||||
const currentScript = document.currentScript;
|
||||
|
||||
/** @param {import('./Docsify').Docsify} vm */
|
||||
export default function(vm) {
|
||||
const config = merge(
|
||||
{
|
||||
|
||||
@ -3,39 +3,47 @@ import { body, on } from '../util/dom';
|
||||
import * as sidebar from './sidebar';
|
||||
import { scrollIntoView, scroll2Top } from './scroll';
|
||||
|
||||
export function eventMixin(proto) {
|
||||
proto.$resetEvents = function(source) {
|
||||
const { auto2top } = this.config;
|
||||
/** @typedef {import('../Docsify').Constructor} Constructor */
|
||||
|
||||
(() => {
|
||||
// Rely on the browser's scroll auto-restoration when going back or forward
|
||||
if (source === 'history') {
|
||||
return;
|
||||
}
|
||||
// Scroll to ID if specified
|
||||
if (this.route.query.id) {
|
||||
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);
|
||||
}
|
||||
})();
|
||||
/**
|
||||
* @template {!Constructor} T
|
||||
* @param {T} Base - The class to extend
|
||||
*/
|
||||
export function Events(Base) {
|
||||
return class Events extends Base {
|
||||
$resetEvents(source) {
|
||||
const { auto2top } = this.config;
|
||||
|
||||
if (this.config.loadNavbar) {
|
||||
sidebar.getAndActive(this.router, 'nav');
|
||||
(() => {
|
||||
// Rely on the browser's scroll auto-restoration when going back or forward
|
||||
if (source === 'history') {
|
||||
return;
|
||||
}
|
||||
// Scroll to ID if specified
|
||||
if (this.route.query.id) {
|
||||
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);
|
||||
}
|
||||
})();
|
||||
|
||||
if (this.config.loadNavbar) {
|
||||
sidebar.getAndActive(this.router, 'nav');
|
||||
}
|
||||
}
|
||||
|
||||
initEvent() {
|
||||
// Bind toggle button
|
||||
sidebar.btn('button.sidebar-toggle', this.router);
|
||||
sidebar.collapse('.sidebar', this.router);
|
||||
// Bind sticky effect
|
||||
if (this.config.coverpage) {
|
||||
!isMobile && on('scroll', sidebar.sticky);
|
||||
} else {
|
||||
body.classList.add('sticky');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function initEvent(vm) {
|
||||
// Bind toggle button
|
||||
sidebar.btn('button.sidebar-toggle', vm.router);
|
||||
sidebar.collapse('.sidebar', vm.router);
|
||||
// Bind sticky effect
|
||||
if (vm.config.coverpage) {
|
||||
!isMobile && on('scroll', sidebar.sticky);
|
||||
} else {
|
||||
body.classList.add('sticky');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { callHook } from '../init/lifecycle';
|
||||
import { getParentPath, stringifyQuery } from '../router/util';
|
||||
import { noop, isExternal } from '../util/core';
|
||||
import { getAndActive } from '../event/sidebar';
|
||||
@ -20,7 +19,13 @@ function loadNested(path, qs, file, next, vm, first) {
|
||||
).then(next, _ => loadNested(path, qs, file, next, vm));
|
||||
}
|
||||
|
||||
export function fetchMixin(proto) {
|
||||
/** @typedef {import('../Docsify').Constructor} Constructor */
|
||||
|
||||
/**
|
||||
* @template {!Constructor} T
|
||||
* @param {T} Base - The class to extend
|
||||
*/
|
||||
export function Fetch(Base) {
|
||||
let last;
|
||||
|
||||
const abort = () => last && last.abort && last.abort();
|
||||
@ -59,44 +64,143 @@ export function fetchMixin(proto) {
|
||||
return path404;
|
||||
};
|
||||
|
||||
proto._loadSideAndNav = function(path, qs, loadSidebar, cb) {
|
||||
return () => {
|
||||
if (!loadSidebar) {
|
||||
return cb();
|
||||
}
|
||||
return class Fetch extends Base {
|
||||
_loadSideAndNav(path, qs, loadSidebar, cb) {
|
||||
return () => {
|
||||
if (!loadSidebar) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
const fn = result => {
|
||||
this._renderSidebar(result);
|
||||
const fn = result => {
|
||||
this._renderSidebar(result);
|
||||
cb();
|
||||
};
|
||||
|
||||
// Load sidebar
|
||||
loadNested(path, qs, loadSidebar, fn, this, true);
|
||||
};
|
||||
}
|
||||
|
||||
_fetch(cb = noop) {
|
||||
const { query } = this.route;
|
||||
let { path } = this.route;
|
||||
|
||||
// Prevent loading remote content via URL hash
|
||||
// Ex: https://foo.com/#//bar.com/file.md
|
||||
if (isExternal(path)) {
|
||||
history.replaceState(null, '', '#');
|
||||
this.router.normalize();
|
||||
} else {
|
||||
const qs = stringifyQuery(query, ['id']);
|
||||
const { loadNavbar, requestHeaders, loadSidebar } = this.config;
|
||||
// Abort last request
|
||||
|
||||
const file = this.router.getFile(path);
|
||||
const req = request(file + qs, true, requestHeaders);
|
||||
|
||||
this.isRemoteUrl = isExternal(file);
|
||||
// Current page is html
|
||||
this.isHTML = /\.html$/g.test(file);
|
||||
|
||||
// Load main content
|
||||
req.then(
|
||||
(text, opt) =>
|
||||
this._renderMain(
|
||||
text,
|
||||
opt,
|
||||
this._loadSideAndNav(path, qs, loadSidebar, cb)
|
||||
),
|
||||
_ => {
|
||||
this._fetchFallbackPage(path, qs, cb) ||
|
||||
this._fetch404(file, qs, cb);
|
||||
}
|
||||
);
|
||||
|
||||
// Load nav
|
||||
loadNavbar &&
|
||||
loadNested(
|
||||
path,
|
||||
qs,
|
||||
loadNavbar,
|
||||
text => this._renderNav(text),
|
||||
this,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_fetchCover() {
|
||||
const { coverpage, requestHeaders } = this.config;
|
||||
const query = this.route.query;
|
||||
const root = getParentPath(this.route.path);
|
||||
|
||||
if (coverpage) {
|
||||
let path = null;
|
||||
const routePath = this.route.path;
|
||||
if (typeof coverpage === 'string') {
|
||||
if (routePath === '/') {
|
||||
path = coverpage;
|
||||
}
|
||||
} else if (Array.isArray(coverpage)) {
|
||||
path = coverpage.indexOf(routePath) > -1 && '_coverpage';
|
||||
} else {
|
||||
const cover = coverpage[routePath];
|
||||
path = cover === true ? '_coverpage' : cover;
|
||||
}
|
||||
|
||||
const coverOnly = Boolean(path) && this.config.onlyCover;
|
||||
if (path) {
|
||||
path = this.router.getFile(root + path);
|
||||
this.coverIsHTML = /\.html$/g.test(path);
|
||||
get(
|
||||
path + stringifyQuery(query, ['id']),
|
||||
false,
|
||||
requestHeaders
|
||||
).then(text => this._renderCover(text, coverOnly));
|
||||
} else {
|
||||
this._renderCover(null, coverOnly);
|
||||
}
|
||||
|
||||
return coverOnly;
|
||||
}
|
||||
}
|
||||
|
||||
$fetch(cb = noop, $resetEvents = this.$resetEvents.bind(this)) {
|
||||
const done = () => {
|
||||
this.callHook('doneEach');
|
||||
cb();
|
||||
};
|
||||
|
||||
// Load sidebar
|
||||
loadNested(path, qs, loadSidebar, fn, this, true);
|
||||
};
|
||||
};
|
||||
const onlyCover = this._fetchCover();
|
||||
|
||||
proto._fetch = function(cb = noop) {
|
||||
const { query } = this.route;
|
||||
let { path } = this.route;
|
||||
if (onlyCover) {
|
||||
done();
|
||||
} else {
|
||||
this._fetch(() => {
|
||||
$resetEvents();
|
||||
done();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent loading remote content via URL hash
|
||||
// Ex: https://foo.com/#//bar.com/file.md
|
||||
if (isExternal(path)) {
|
||||
history.replaceState(null, '', '#');
|
||||
this.router.normalize();
|
||||
} else {
|
||||
const qs = stringifyQuery(query, ['id']);
|
||||
const { loadNavbar, requestHeaders, loadSidebar } = this.config;
|
||||
// Abort last request
|
||||
_fetchFallbackPage(path, qs, cb = noop) {
|
||||
const { requestHeaders, fallbackLanguages, loadSidebar } = this.config;
|
||||
|
||||
const file = this.router.getFile(path);
|
||||
const req = request(file + qs, true, requestHeaders);
|
||||
if (!fallbackLanguages) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.isRemoteUrl = isExternal(file);
|
||||
// Current page is html
|
||||
this.isHTML = /\.html$/g.test(file);
|
||||
const local = path.split('/')[1];
|
||||
|
||||
if (fallbackLanguages.indexOf(local) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newPath = this.router.getFile(
|
||||
path.replace(new RegExp(`^/${local}`), '')
|
||||
);
|
||||
const req = request(newPath + qs, true, requestHeaders);
|
||||
|
||||
// Load main content
|
||||
req.then(
|
||||
(text, opt) =>
|
||||
this._renderMain(
|
||||
@ -104,154 +208,55 @@ export function fetchMixin(proto) {
|
||||
opt,
|
||||
this._loadSideAndNav(path, qs, loadSidebar, cb)
|
||||
),
|
||||
_ => {
|
||||
this._fetchFallbackPage(path, qs, cb) || this._fetch404(file, qs, cb);
|
||||
}
|
||||
() => this._fetch404(path, qs, cb)
|
||||
);
|
||||
|
||||
// Load nav
|
||||
loadNavbar &&
|
||||
loadNested(
|
||||
path,
|
||||
qs,
|
||||
loadNavbar,
|
||||
text => this._renderNav(text),
|
||||
this,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
proto._fetchCover = function() {
|
||||
const { coverpage, requestHeaders } = this.config;
|
||||
const query = this.route.query;
|
||||
const root = getParentPath(this.route.path);
|
||||
|
||||
if (coverpage) {
|
||||
let path = null;
|
||||
const routePath = this.route.path;
|
||||
if (typeof coverpage === 'string') {
|
||||
if (routePath === '/') {
|
||||
path = coverpage;
|
||||
}
|
||||
} else if (Array.isArray(coverpage)) {
|
||||
path = coverpage.indexOf(routePath) > -1 && '_coverpage';
|
||||
} else {
|
||||
const cover = coverpage[routePath];
|
||||
path = cover === true ? '_coverpage' : cover;
|
||||
}
|
||||
|
||||
const coverOnly = Boolean(path) && this.config.onlyCover;
|
||||
if (path) {
|
||||
path = this.router.getFile(root + path);
|
||||
this.coverIsHTML = /\.html$/g.test(path);
|
||||
get(
|
||||
path + stringifyQuery(query, ['id']),
|
||||
false,
|
||||
requestHeaders
|
||||
).then(text => this._renderCover(text, coverOnly));
|
||||
} else {
|
||||
this._renderCover(null, coverOnly);
|
||||
}
|
||||
|
||||
return coverOnly;
|
||||
}
|
||||
};
|
||||
|
||||
proto.$fetch = function(
|
||||
cb = noop,
|
||||
$resetEvents = this.$resetEvents.bind(this)
|
||||
) {
|
||||
const done = () => {
|
||||
callHook(this, 'doneEach');
|
||||
cb();
|
||||
};
|
||||
|
||||
const onlyCover = this._fetchCover();
|
||||
|
||||
if (onlyCover) {
|
||||
done();
|
||||
} else {
|
||||
this._fetch(() => {
|
||||
$resetEvents();
|
||||
done();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
proto._fetchFallbackPage = function(path, qs, cb = noop) {
|
||||
const { requestHeaders, fallbackLanguages, loadSidebar } = this.config;
|
||||
|
||||
if (!fallbackLanguages) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const local = path.split('/')[1];
|
||||
|
||||
if (fallbackLanguages.indexOf(local) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newPath = this.router.getFile(
|
||||
path.replace(new RegExp(`^/${local}`), '')
|
||||
);
|
||||
const req = request(newPath + qs, true, requestHeaders);
|
||||
|
||||
req.then(
|
||||
(text, opt) =>
|
||||
this._renderMain(
|
||||
text,
|
||||
opt,
|
||||
this._loadSideAndNav(path, qs, loadSidebar, cb)
|
||||
),
|
||||
() => this._fetch404(path, qs, cb)
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the 404 page
|
||||
* @param {String} path URL to be loaded
|
||||
* @param {*} qs TODO: define
|
||||
* @param {Function} cb Callback
|
||||
* @returns {Boolean} True if the requested page is not found
|
||||
* @private
|
||||
*/
|
||||
proto._fetch404 = function(path, qs, cb = noop) {
|
||||
const { loadSidebar, requestHeaders, notFoundPage } = this.config;
|
||||
|
||||
const fnLoadSideAndNav = this._loadSideAndNav(path, qs, loadSidebar, cb);
|
||||
if (notFoundPage) {
|
||||
const path404 = get404Path(path, this.config);
|
||||
|
||||
request(this.router.getFile(path404), true, requestHeaders).then(
|
||||
(text, opt) => this._renderMain(text, opt, fnLoadSideAndNav),
|
||||
() => this._renderMain(null, {}, fnLoadSideAndNav)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
this._renderMain(null, {}, fnLoadSideAndNav);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Load the 404 page
|
||||
* @param {String} path URL to be loaded
|
||||
* @param {*} qs TODO: define
|
||||
* @param {Function} cb Callback
|
||||
* @returns {Boolean} True if the requested page is not found
|
||||
* @private
|
||||
*/
|
||||
_fetch404(path, qs, cb = noop) {
|
||||
const { loadSidebar, requestHeaders, notFoundPage } = this.config;
|
||||
|
||||
export function initFetch(vm) {
|
||||
const { loadSidebar } = vm.config;
|
||||
const fnLoadSideAndNav = this._loadSideAndNav(path, qs, loadSidebar, cb);
|
||||
if (notFoundPage) {
|
||||
const path404 = get404Path(path, this.config);
|
||||
|
||||
// Server-Side Rendering
|
||||
if (vm.rendered) {
|
||||
const activeEl = getAndActive(vm.router, '.sidebar-nav', true, true);
|
||||
if (loadSidebar && activeEl) {
|
||||
activeEl.parentNode.innerHTML += window.__SUB_SIDEBAR__;
|
||||
request(this.router.getFile(path404), true, requestHeaders).then(
|
||||
(text, opt) => this._renderMain(text, opt, fnLoadSideAndNav),
|
||||
() => this._renderMain(null, {}, fnLoadSideAndNav)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
this._renderMain(null, {}, fnLoadSideAndNav);
|
||||
return false;
|
||||
}
|
||||
|
||||
vm._bindEventOnRendered(activeEl);
|
||||
vm.$resetEvents();
|
||||
callHook(vm, 'doneEach');
|
||||
callHook(vm, 'ready');
|
||||
} else {
|
||||
vm.$fetch(_ => callHook(vm, 'ready'));
|
||||
}
|
||||
initFetch() {
|
||||
const { loadSidebar } = this.config;
|
||||
|
||||
// Server-Side Rendering
|
||||
if (this.rendered) {
|
||||
const activeEl = getAndActive(this.router, '.sidebar-nav', true, true);
|
||||
if (loadSidebar && activeEl) {
|
||||
activeEl.parentNode.innerHTML += window.__SUB_SIDEBAR__;
|
||||
}
|
||||
|
||||
this._bindEventOnRendered(activeEl);
|
||||
this.$resetEvents();
|
||||
this.callHook('doneEach');
|
||||
this.callHook('ready');
|
||||
} else {
|
||||
this.$fetch(_ => this.callHook('ready'));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import config from '../config';
|
||||
import { initRender } from '../render';
|
||||
import { initRouter } from '../router';
|
||||
import { initEvent } from '../event';
|
||||
import { initFetch } from '../fetch';
|
||||
import { isFn } from '../util/core';
|
||||
import { initLifecycle, callHook } from './lifecycle';
|
||||
|
||||
export function initMixin(proto) {
|
||||
proto._init = function() {
|
||||
const vm = this;
|
||||
vm.config = config(vm);
|
||||
|
||||
initLifecycle(vm); // Init hooks
|
||||
initPlugin(vm); // Install plugins
|
||||
callHook(vm, 'init');
|
||||
initRouter(vm); // Add router
|
||||
initRender(vm); // Render base DOM
|
||||
initEvent(vm); // Bind events
|
||||
initFetch(vm); // Fetch data
|
||||
callHook(vm, 'mounted');
|
||||
};
|
||||
}
|
||||
|
||||
function initPlugin(vm) {
|
||||
[].concat(vm.config.plugins).forEach(fn => isFn(fn) && fn(vm._lifecycle, vm));
|
||||
}
|
||||
@ -1,46 +1,57 @@
|
||||
import { noop } from '../util/core';
|
||||
|
||||
export function initLifecycle(vm) {
|
||||
const hooks = [
|
||||
'init',
|
||||
'mounted',
|
||||
'beforeEach',
|
||||
'afterEach',
|
||||
'doneEach',
|
||||
'ready',
|
||||
];
|
||||
/** @typedef {import('../Docsify').Constructor} Constructor */
|
||||
|
||||
vm._hooks = {};
|
||||
vm._lifecycle = {};
|
||||
hooks.forEach(hook => {
|
||||
const arr = (vm._hooks[hook] = []);
|
||||
vm._lifecycle[hook] = fn => arr.push(fn);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @template {!Constructor} T
|
||||
* @param {T} Base - The class to extend
|
||||
*/
|
||||
export function Lifecycle(Base) {
|
||||
return class Lifecycle extends Base {
|
||||
initLifecycle() {
|
||||
const hooks = [
|
||||
'init',
|
||||
'mounted',
|
||||
'beforeEach',
|
||||
'afterEach',
|
||||
'doneEach',
|
||||
'ready',
|
||||
];
|
||||
|
||||
export function callHook(vm, hookName, data, next = noop) {
|
||||
const queue = vm._hooks[hookName];
|
||||
this._hooks = {};
|
||||
this._lifecycle = {};
|
||||
|
||||
const step = function(index) {
|
||||
const hookFn = queue[index];
|
||||
hooks.forEach(hook => {
|
||||
const arr = (this._hooks[hook] = []);
|
||||
this._lifecycle[hook] = fn => arr.push(fn);
|
||||
});
|
||||
}
|
||||
|
||||
if (index >= queue.length) {
|
||||
next(data);
|
||||
} else if (typeof hookFn === 'function') {
|
||||
if (hookFn.length === 2) {
|
||||
hookFn(data, result => {
|
||||
data = result;
|
||||
callHook(hookName, data, next = noop) {
|
||||
const queue = this._hooks[hookName];
|
||||
|
||||
const step = function(index) {
|
||||
const hookFn = queue[index];
|
||||
|
||||
if (index >= queue.length) {
|
||||
next(data);
|
||||
} else if (typeof hookFn === 'function') {
|
||||
if (hookFn.length === 2) {
|
||||
hookFn(data, result => {
|
||||
data = result;
|
||||
step(index + 1);
|
||||
});
|
||||
} else {
|
||||
const result = hookFn(data);
|
||||
data = result === undefined ? data : result;
|
||||
step(index + 1);
|
||||
}
|
||||
} else {
|
||||
step(index + 1);
|
||||
});
|
||||
} else {
|
||||
const result = hookFn(data);
|
||||
data = result === undefined ? data : result;
|
||||
step(index + 1);
|
||||
}
|
||||
} else {
|
||||
step(index + 1);
|
||||
}
|
||||
};
|
||||
|
||||
step(0);
|
||||
}
|
||||
};
|
||||
|
||||
step(0);
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ import tinydate from 'tinydate';
|
||||
import DOMPurify from 'dompurify';
|
||||
import * as dom from '../util/dom';
|
||||
import cssVars from '../util/polyfill/css-vars';
|
||||
import { callHook } from '../init/lifecycle';
|
||||
import { getAndActive, sticky } from '../event/sidebar';
|
||||
import { getPath, isAbsolutePath } from '../router/util';
|
||||
import { isMobile, inBrowser } from '../util/env';
|
||||
@ -239,223 +238,235 @@ function renderNameLink(vm) {
|
||||
}
|
||||
}
|
||||
|
||||
export function renderMixin(proto) {
|
||||
proto._renderTo = function(el, content, replace) {
|
||||
const node = dom.getNode(el);
|
||||
if (node) {
|
||||
node[replace ? 'outerHTML' : 'innerHTML'] = content;
|
||||
}
|
||||
};
|
||||
/** @typedef {import('../Docsify').Constructor} Constructor */
|
||||
|
||||
proto._renderSidebar = function(text) {
|
||||
const { maxLevel, subMaxLevel, loadSidebar, hideSidebar } = this.config;
|
||||
|
||||
if (hideSidebar) {
|
||||
// FIXME : better styling solution
|
||||
[
|
||||
document.querySelector('aside.sidebar'),
|
||||
document.querySelector('button.sidebar-toggle'),
|
||||
].forEach(node => node.parentNode.removeChild(node));
|
||||
document.querySelector('section.content').style.right = 'unset';
|
||||
document.querySelector('section.content').style.left = 'unset';
|
||||
document.querySelector('section.content').style.position = 'relative';
|
||||
document.querySelector('section.content').style.width = '100%';
|
||||
return null;
|
||||
}
|
||||
|
||||
this._renderTo('.sidebar-nav', this.compiler.sidebar(text, maxLevel));
|
||||
const activeEl = getAndActive(this.router, '.sidebar-nav', true, true);
|
||||
if (loadSidebar && activeEl) {
|
||||
activeEl.parentNode.innerHTML +=
|
||||
this.compiler.subSidebar(subMaxLevel) || '';
|
||||
} else {
|
||||
// Reset toc
|
||||
this.compiler.subSidebar();
|
||||
}
|
||||
|
||||
// Bind event
|
||||
this._bindEventOnRendered(activeEl);
|
||||
};
|
||||
|
||||
proto._bindEventOnRendered = function(activeEl) {
|
||||
const { autoHeader } = this.config;
|
||||
|
||||
scrollActiveSidebar(this.router);
|
||||
|
||||
if (autoHeader && activeEl) {
|
||||
const main = dom.getNode('#main');
|
||||
const firstNode = main.children[0];
|
||||
if (firstNode && firstNode.tagName !== 'H1') {
|
||||
const h1 = this.compiler.header(activeEl.innerText, 1);
|
||||
const wrapper = dom.create('div', h1);
|
||||
dom.before(main, wrapper.children[0]);
|
||||
/**
|
||||
* @template {!Constructor} T
|
||||
* @param {T} Base - The class to extend
|
||||
*/
|
||||
export function Render(Base) {
|
||||
return class Render extends Base {
|
||||
_renderTo(el, content, replace) {
|
||||
const node = dom.getNode(el);
|
||||
if (node) {
|
||||
node[replace ? 'outerHTML' : 'innerHTML'] = content;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
proto._renderNav = function(text) {
|
||||
text && this._renderTo('nav', this.compiler.compile(text));
|
||||
if (this.config.loadNavbar) {
|
||||
getAndActive(this.router, 'nav');
|
||||
}
|
||||
};
|
||||
_renderSidebar(text) {
|
||||
const { maxLevel, subMaxLevel, loadSidebar, hideSidebar } = this.config;
|
||||
|
||||
proto._renderMain = function(text, opt = {}, next) {
|
||||
if (!text) {
|
||||
return renderMain.call(this, text);
|
||||
}
|
||||
if (hideSidebar) {
|
||||
// FIXME : better styling solution
|
||||
[
|
||||
document.querySelector('aside.sidebar'),
|
||||
document.querySelector('button.sidebar-toggle'),
|
||||
].forEach(node => node.parentNode.removeChild(node));
|
||||
document.querySelector('section.content').style.right = 'unset';
|
||||
document.querySelector('section.content').style.left = 'unset';
|
||||
document.querySelector('section.content').style.position = 'relative';
|
||||
document.querySelector('section.content').style.width = '100%';
|
||||
return null;
|
||||
}
|
||||
|
||||
callHook(this, 'beforeEach', text, result => {
|
||||
let html;
|
||||
const callback = () => {
|
||||
if (opt.updatedAt) {
|
||||
html = formatUpdated(html, opt.updatedAt, this.config.formatUpdated);
|
||||
}
|
||||
|
||||
callHook(this, 'afterEach', html, hookData =>
|
||||
renderMain.call(this, hookData)
|
||||
);
|
||||
};
|
||||
|
||||
if (this.isHTML) {
|
||||
html = this.result = text;
|
||||
callback();
|
||||
next();
|
||||
this._renderTo('.sidebar-nav', this.compiler.sidebar(text, maxLevel));
|
||||
const activeEl = getAndActive(this.router, '.sidebar-nav', true, true);
|
||||
if (loadSidebar && activeEl) {
|
||||
activeEl.parentNode.innerHTML +=
|
||||
this.compiler.subSidebar(subMaxLevel) || '';
|
||||
} else {
|
||||
prerenderEmbed(
|
||||
{
|
||||
compiler: this.compiler,
|
||||
raw: result,
|
||||
},
|
||||
tokens => {
|
||||
html = this.compiler.compile(tokens);
|
||||
html = this.isRemoteUrl
|
||||
? DOMPurify.sanitize(html, { ADD_TAGS: ['script'] })
|
||||
: html;
|
||||
callback();
|
||||
next();
|
||||
// Reset toc
|
||||
this.compiler.subSidebar();
|
||||
}
|
||||
|
||||
// Bind event
|
||||
this._bindEventOnRendered(activeEl);
|
||||
}
|
||||
|
||||
_bindEventOnRendered(activeEl) {
|
||||
const { autoHeader } = this.config;
|
||||
|
||||
scrollActiveSidebar(this.router);
|
||||
|
||||
if (autoHeader && activeEl) {
|
||||
const main = dom.getNode('#main');
|
||||
const firstNode = main.children[0];
|
||||
if (firstNode && firstNode.tagName !== 'H1') {
|
||||
const h1 = this.compiler.header(activeEl.innerText, 1);
|
||||
const wrapper = dom.create('div', h1);
|
||||
dom.before(main, wrapper.children[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_renderNav(text) {
|
||||
text && this._renderTo('nav', this.compiler.compile(text));
|
||||
if (this.config.loadNavbar) {
|
||||
getAndActive(this.router, 'nav');
|
||||
}
|
||||
}
|
||||
|
||||
_renderMain(text, opt = {}, next) {
|
||||
if (!text) {
|
||||
return renderMain.call(this, text);
|
||||
}
|
||||
|
||||
this.callHook('beforeEach', text, result => {
|
||||
let html;
|
||||
const callback = () => {
|
||||
if (opt.updatedAt) {
|
||||
html = formatUpdated(
|
||||
html,
|
||||
opt.updatedAt,
|
||||
this.config.formatUpdated
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
proto._renderCover = function(text, coverOnly) {
|
||||
const el = dom.getNode('.cover');
|
||||
this.callHook('afterEach', html, hookData =>
|
||||
renderMain.call(this, hookData)
|
||||
);
|
||||
};
|
||||
|
||||
dom.toggleClass(
|
||||
dom.getNode('main'),
|
||||
coverOnly ? 'add' : 'remove',
|
||||
'hidden'
|
||||
);
|
||||
if (!text) {
|
||||
dom.toggleClass(el, 'remove', 'show');
|
||||
return;
|
||||
if (this.isHTML) {
|
||||
html = this.result = text;
|
||||
callback();
|
||||
next();
|
||||
} else {
|
||||
prerenderEmbed(
|
||||
{
|
||||
compiler: this.compiler,
|
||||
raw: result,
|
||||
},
|
||||
tokens => {
|
||||
html = this.compiler.compile(tokens);
|
||||
html = this.isRemoteUrl
|
||||
? DOMPurify.sanitize(html, { ADD_TAGS: ['script'] })
|
||||
: html;
|
||||
callback();
|
||||
next();
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dom.toggleClass(el, 'add', 'show');
|
||||
_renderCover(text, coverOnly) {
|
||||
const el = dom.getNode('.cover');
|
||||
|
||||
let html = this.coverIsHTML ? text : this.compiler.cover(text);
|
||||
dom.toggleClass(
|
||||
dom.getNode('main'),
|
||||
coverOnly ? 'add' : 'remove',
|
||||
'hidden'
|
||||
);
|
||||
if (!text) {
|
||||
dom.toggleClass(el, 'remove', 'show');
|
||||
return;
|
||||
}
|
||||
|
||||
const m = html
|
||||
.trim()
|
||||
.match('<p><img.*?data-origin="(.*?)"[^a]+alt="(.*?)">([^<]*?)</p>$');
|
||||
dom.toggleClass(el, 'add', 'show');
|
||||
|
||||
if (m) {
|
||||
if (m[2] === 'color') {
|
||||
el.style.background = m[1] + (m[3] || '');
|
||||
} else {
|
||||
let path = m[1];
|
||||
let html = this.coverIsHTML ? text : this.compiler.cover(text);
|
||||
|
||||
dom.toggleClass(el, 'add', 'has-mask');
|
||||
if (!isAbsolutePath(m[1])) {
|
||||
path = getPath(this.router.getBasePath(), m[1]);
|
||||
const m = html
|
||||
.trim()
|
||||
.match('<p><img.*?data-origin="(.*?)"[^a]+alt="(.*?)">([^<]*?)</p>$');
|
||||
|
||||
if (m) {
|
||||
if (m[2] === 'color') {
|
||||
el.style.background = m[1] + (m[3] || '');
|
||||
} else {
|
||||
let path = m[1];
|
||||
|
||||
dom.toggleClass(el, 'add', 'has-mask');
|
||||
if (!isAbsolutePath(m[1])) {
|
||||
path = getPath(this.router.getBasePath(), m[1]);
|
||||
}
|
||||
|
||||
el.style.backgroundImage = `url(${path})`;
|
||||
el.style.backgroundSize = 'cover';
|
||||
el.style.backgroundPosition = 'center center';
|
||||
}
|
||||
|
||||
el.style.backgroundImage = `url(${path})`;
|
||||
el.style.backgroundSize = 'cover';
|
||||
el.style.backgroundPosition = 'center center';
|
||||
html = html.replace(m[0], '');
|
||||
}
|
||||
|
||||
html = html.replace(m[0], '');
|
||||
this._renderTo('.cover-main', html);
|
||||
sticky();
|
||||
}
|
||||
|
||||
this._renderTo('.cover-main', html);
|
||||
sticky();
|
||||
};
|
||||
|
||||
proto._updateRender = function() {
|
||||
// Render name link
|
||||
renderNameLink(this);
|
||||
};
|
||||
}
|
||||
|
||||
export function initRender(vm) {
|
||||
const config = vm.config;
|
||||
|
||||
// Init markdown compiler
|
||||
vm.compiler = new Compiler(config, vm.router);
|
||||
if (inBrowser) {
|
||||
/* eslint-disable-next-line camelcase */
|
||||
window.__current_docsify_compiler__ = vm.compiler;
|
||||
}
|
||||
|
||||
const id = config.el || '#app';
|
||||
const navEl = dom.find('nav') || dom.create('nav');
|
||||
|
||||
const el = dom.find(id);
|
||||
let html = '';
|
||||
let navAppendToTarget = dom.body;
|
||||
|
||||
if (el) {
|
||||
if (config.repo) {
|
||||
html += tpl.corner(config.repo, config.cornerExternalLinkTarge);
|
||||
_updateRender() {
|
||||
// Render name link
|
||||
renderNameLink(this);
|
||||
}
|
||||
|
||||
if (config.coverpage) {
|
||||
html += tpl.cover();
|
||||
}
|
||||
initRender() {
|
||||
const config = this.config;
|
||||
|
||||
if (config.logo) {
|
||||
const isBase64 = /^data:image/.test(config.logo);
|
||||
const isExternal = /(?:http[s]?:)?\/\//.test(config.logo);
|
||||
const isRelative = /^\./.test(config.logo);
|
||||
|
||||
if (!isBase64 && !isExternal && !isRelative) {
|
||||
config.logo = getPath(vm.router.getBasePath(), config.logo);
|
||||
// Init markdown compiler
|
||||
this.compiler = new Compiler(config, this.router);
|
||||
if (inBrowser) {
|
||||
/* eslint-disable-next-line camelcase */
|
||||
window.__current_docsify_compiler__ = this.compiler;
|
||||
}
|
||||
|
||||
const id = config.el || '#app';
|
||||
const navEl = dom.find('nav') || dom.create('nav');
|
||||
|
||||
const el = dom.find(id);
|
||||
let html = '';
|
||||
let navAppendToTarget = dom.body;
|
||||
|
||||
if (el) {
|
||||
if (config.repo) {
|
||||
html += tpl.corner(config.repo, config.cornerExternalLinkTarge);
|
||||
}
|
||||
|
||||
if (config.coverpage) {
|
||||
html += tpl.cover();
|
||||
}
|
||||
|
||||
if (config.logo) {
|
||||
const isBase64 = /^data:image/.test(config.logo);
|
||||
const isExternal = /(?:http[s]?:)?\/\//.test(config.logo);
|
||||
const isRelative = /^\./.test(config.logo);
|
||||
|
||||
if (!isBase64 && !isExternal && !isRelative) {
|
||||
config.logo = getPath(this.router.getBasePath(), config.logo);
|
||||
}
|
||||
}
|
||||
|
||||
html += tpl.main(config);
|
||||
// Render main app
|
||||
this._renderTo(el, html, true);
|
||||
} else {
|
||||
this.rendered = true;
|
||||
}
|
||||
|
||||
if (config.mergeNavbar && isMobile) {
|
||||
navAppendToTarget = dom.find('.sidebar');
|
||||
} else {
|
||||
navEl.classList.add('app-nav');
|
||||
|
||||
if (!config.repo) {
|
||||
navEl.classList.add('no-badge');
|
||||
}
|
||||
}
|
||||
|
||||
// Add nav
|
||||
if (config.loadNavbar) {
|
||||
dom.before(navAppendToTarget, navEl);
|
||||
}
|
||||
|
||||
if (config.themeColor) {
|
||||
dom.$.head.appendChild(
|
||||
dom.create('div', tpl.theme(config.themeColor)).firstElementChild
|
||||
);
|
||||
// Polyfll
|
||||
cssVars(config.themeColor);
|
||||
}
|
||||
|
||||
this._updateRender();
|
||||
dom.toggleClass(dom.body, 'ready');
|
||||
}
|
||||
|
||||
html += tpl.main(config);
|
||||
// Render main app
|
||||
vm._renderTo(el, html, true);
|
||||
} else {
|
||||
vm.rendered = true;
|
||||
}
|
||||
|
||||
if (config.mergeNavbar && isMobile) {
|
||||
navAppendToTarget = dom.find('.sidebar');
|
||||
} else {
|
||||
navEl.classList.add('app-nav');
|
||||
|
||||
if (!config.repo) {
|
||||
navEl.classList.add('no-badge');
|
||||
}
|
||||
}
|
||||
|
||||
// Add nav
|
||||
if (config.loadNavbar) {
|
||||
dom.before(navAppendToTarget, navEl);
|
||||
}
|
||||
|
||||
if (config.themeColor) {
|
||||
dom.$.head.appendChild(
|
||||
dom.create('div', tpl.theme(config.themeColor)).firstElementChild
|
||||
);
|
||||
// Polyfll
|
||||
cssVars(config.themeColor);
|
||||
}
|
||||
|
||||
vm._updateRender();
|
||||
dom.toggleClass(dom.body, 'ready');
|
||||
};
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ export class HashHistory extends History {
|
||||
return index === -1 ? '' : href.slice(index + 1);
|
||||
}
|
||||
|
||||
/** @param {((params: {source: TODO}) => void)} [cb] */
|
||||
onchange(cb = noop) {
|
||||
// The hashchange event does not tell us if it originated from
|
||||
// a clicked link or by moving back/forward in the history;
|
||||
@ -100,3 +101,5 @@ export class HashHistory extends History {
|
||||
return '#' + super.toURL(path, params, currentRoute);
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
||||
@ -4,44 +4,64 @@ import { noop } from '../util/core';
|
||||
import { HashHistory } from './history/hash';
|
||||
import { HTML5History } from './history/html5';
|
||||
|
||||
export function routerMixin(proto) {
|
||||
proto.route = {};
|
||||
}
|
||||
/**
|
||||
* @typedef {{
|
||||
* path?: string
|
||||
* }} Route
|
||||
*/
|
||||
|
||||
/** @type {Route} */
|
||||
let lastRoute = {};
|
||||
|
||||
function updateRender(vm) {
|
||||
vm.router.normalize();
|
||||
vm.route = vm.router.parse();
|
||||
dom.body.setAttribute('data-page', vm.route.file);
|
||||
}
|
||||
/** @typedef {import('../Docsify').Constructor} Constructor */
|
||||
|
||||
export function initRouter(vm) {
|
||||
const config = vm.config;
|
||||
const mode = config.routerMode || 'hash';
|
||||
let router;
|
||||
/**
|
||||
* @template {!Constructor} T
|
||||
* @param {T} Base - The class to extend
|
||||
*/
|
||||
export function Router(Base) {
|
||||
return class Router extends Base {
|
||||
/** @param {any[]} args */
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
if (mode === 'history' && supportsPushState) {
|
||||
router = new HTML5History(config);
|
||||
} else {
|
||||
router = new HashHistory(config);
|
||||
}
|
||||
|
||||
vm.router = router;
|
||||
updateRender(vm);
|
||||
lastRoute = vm.route;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
router.onchange(params => {
|
||||
updateRender(vm);
|
||||
vm._updateRender();
|
||||
|
||||
if (lastRoute.path === vm.route.path) {
|
||||
vm.$resetEvents(params.source);
|
||||
return;
|
||||
this.route = {};
|
||||
}
|
||||
|
||||
vm.$fetch(noop, vm.$resetEvents.bind(vm, params.source));
|
||||
lastRoute = vm.route;
|
||||
});
|
||||
updateRender() {
|
||||
this.router.normalize();
|
||||
this.route = this.router.parse();
|
||||
dom.body.setAttribute('data-page', this.route.file);
|
||||
}
|
||||
|
||||
initRouter() {
|
||||
const config = this.config;
|
||||
const mode = config.routerMode || 'hash';
|
||||
let router;
|
||||
|
||||
if (mode === 'history' && supportsPushState) {
|
||||
router = new HTML5History(config);
|
||||
} else {
|
||||
router = new HashHistory(config);
|
||||
}
|
||||
|
||||
this.router = router;
|
||||
this.updateRender();
|
||||
lastRoute = this.route;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
router.onchange(params => {
|
||||
this.updateRender();
|
||||
this._updateRender();
|
||||
|
||||
if (lastRoute.path === this.route.path) {
|
||||
this.$resetEvents(params.source);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$fetch(noop, this.$resetEvents.bind(this, params.source));
|
||||
lastRoute = this.route;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -56,8 +56,8 @@ describe('Sidebar Tests', function() {
|
||||
expect(page.url()).toMatch(/\/test-foo$/);
|
||||
|
||||
await page.click('a[href="#/test.foo"]');
|
||||
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test .');
|
||||
expect(page.url()).toMatch(/\/test.foo$/);
|
||||
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test .');
|
||||
|
||||
await page.click('a[href="#/test>foo"]');
|
||||
await expect(page).toEqualText('.sidebar-nav li[class=active]', 'Test >');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user