diff --git a/src/core/Docsify.js b/src/core/Docsify.js index fcb40336bf..ba6c54b88f 100644 --- a/src/core/Docsify.js +++ b/src/core/Docsify.js @@ -1,21 +1,38 @@ -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(); -const proto = Docsify.prototype; + this.config = config(this); -initMixin(proto); -routerMixin(proto); -renderMixin(proto); -fetchMixin(proto); -eventMixin(proto); + 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)); + } +} /** * Global API diff --git a/src/core/config.js b/src/core/config.js index 2d23f5db29..1aa69a7965 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -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( { diff --git a/src/core/event/index.js b/src/core/event/index.js index 4a312421ea..11c02f9754 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -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; + + (() => { + // 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'); } - })(); + } - 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'); - } -} diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index f0f2887986..552ec072ed 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -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); + 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; } + } - const fn = result => { - this._renderSidebar(result); + $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(); + + if (onlyCover) { + done(); + } else { + this._fetch(() => { + $resetEvents(); + done(); + }); + } + } + + _fetchFallbackPage(path, qs, cb = noop) { + const { requestHeaders, fallbackLanguages, loadSidebar } = this.config; - proto._fetch = function(cb = noop) { - const { query } = this.route; - let { path } = this.route; + if (!fallbackLanguages) { + return false; + } - // 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 local = path.split('/')[1]; - const file = this.router.getFile(path); - const req = request(file + qs, true, requestHeaders); + if (fallbackLanguages.indexOf(local) === -1) { + return false; + } - this.isRemoteUrl = isExternal(file); - // Current page is html - this.isHTML = /\.html$/g.test(file); + 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 - ); + return 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); + /** + * 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; + + 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; } - 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) { + this._renderMain(null, {}, fnLoadSideAndNav); return false; } - const local = path.split('/')[1]; + initFetch() { + const { loadSidebar } = this.config; - 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; - }; + // Server-Side Rendering + if (this.rendered) { + const activeEl = getAndActive(this.router, '.sidebar-nav', true, true); + if (loadSidebar && activeEl) { + activeEl.parentNode.innerHTML += window.__SUB_SIDEBAR__; + } - /** - * 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._bindEventOnRendered(activeEl); + this.$resetEvents(); + this.callHook('doneEach'); + this.callHook('ready'); + } else { + this.$fetch(_ => this.callHook('ready')); + } } - - this._renderMain(null, {}, fnLoadSideAndNav); - return false; }; } - -export function initFetch(vm) { - const { loadSidebar } = vm.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__; - } - - vm._bindEventOnRendered(activeEl); - vm.$resetEvents(); - callHook(vm, 'doneEach'); - callHook(vm, 'ready'); - } else { - vm.$fetch(_ => callHook(vm, 'ready')); - } -} diff --git a/src/core/init/index.js b/src/core/init/index.js deleted file mode 100644 index 255aad06c3..0000000000 --- a/src/core/init/index.js +++ /dev/null @@ -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)); -} diff --git a/src/core/init/lifecycle.js b/src/core/init/lifecycle.js index d695ed6e68..38c0dd785b 100644 --- a/src/core/init/lifecycle.js +++ b/src/core/init/lifecycle.js @@ -1,46 +1,57 @@ import { noop } from '../util/core'; -export function initLifecycle(vm) { - const hooks = [ - 'init', - 'mounted', - 'beforeEach', - 'afterEach', - 'doneEach', - 'ready', - ]; - - vm._hooks = {}; - vm._lifecycle = {}; - hooks.forEach(hook => { - const arr = (vm._hooks[hook] = []); - vm._lifecycle[hook] = fn => arr.push(fn); - }); -} +/** @typedef {import('../Docsify').Constructor} Constructor */ + +/** + * @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 = {}; + + hooks.forEach(hook => { + const arr = (this._hooks[hook] = []); + this._lifecycle[hook] = fn => arr.push(fn); + }); + } - const step = function(index) { - const hookFn = queue[index]; + callHook(hookName, data, next = noop) { + const queue = this._hooks[hookName]; - if (index >= queue.length) { - next(data); - } else if (typeof hookFn === 'function') { - if (hookFn.length === 2) { - hookFn(data, result => { - data = result; + 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); } diff --git a/src/core/render/index.js b/src/core/render/index.js index 9fb777baf8..d0e6c46268 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -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 */ + +/** + * @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._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; - } + _renderSidebar(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(); - } + 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); - }; + // Bind event + this._bindEventOnRendered(activeEl); + } - proto._bindEventOnRendered = function(activeEl) { - const { autoHeader } = this.config; + _bindEventOnRendered(activeEl) { + const { autoHeader } = this.config; - scrollActiveSidebar(this.router); + 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]); + 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]); + } } } - }; - proto._renderNav = function(text) { - text && this._renderTo('nav', this.compiler.compile(text)); - if (this.config.loadNavbar) { - getAndActive(this.router, 'nav'); + _renderNav(text) { + text && this._renderTo('nav', this.compiler.compile(text)); + if (this.config.loadNavbar) { + getAndActive(this.router, 'nav'); + } } - }; - proto._renderMain = function(text, opt = {}, next) { - if (!text) { - return renderMain.call(this, text); - } + _renderMain(text, opt = {}, next) { + if (!text) { + return renderMain.call(this, text); + } - callHook(this, 'beforeEach', text, result => { - let html; - const callback = () => { - if (opt.updatedAt) { - html = formatUpdated(html, opt.updatedAt, this.config.formatUpdated); + this.callHook('beforeEach', text, result => { + let html; + const callback = () => { + if (opt.updatedAt) { + html = formatUpdated( + html, + opt.updatedAt, + this.config.formatUpdated + ); + } + + this.callHook('afterEach', html, hookData => + renderMain.call(this, hookData) + ); + }; + + 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(); + } + ); } + }); + } - callHook(this, 'afterEach', html, hookData => - renderMain.call(this, hookData) - ); - }; + _renderCover(text, coverOnly) { + const el = dom.getNode('.cover'); - 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( + dom.getNode('main'), + coverOnly ? 'add' : 'remove', + 'hidden' + ); + if (!text) { + dom.toggleClass(el, 'remove', 'show'); + return; } - }); - }; - proto._renderCover = function(text, coverOnly) { - const el = dom.getNode('.cover'); + dom.toggleClass(el, 'add', 'show'); - dom.toggleClass( - dom.getNode('main'), - coverOnly ? 'add' : 'remove', - 'hidden' - ); - if (!text) { - dom.toggleClass(el, 'remove', 'show'); - return; - } - - dom.toggleClass(el, 'add', 'show'); + let html = this.coverIsHTML ? text : this.compiler.cover(text); - let html = this.coverIsHTML ? text : this.compiler.cover(text); + const m = html + .trim() + .match('

([^<]*?)

$'); - const m = html - .trim() - .match('

([^<]*?)

$'); + if (m) { + if (m[2] === 'color') { + el.style.background = m[1] + (m[3] || ''); + } else { + let path = m[1]; - 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]); + } - 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(); - }; + _updateRender() { + // Render name link + renderNameLink(this); + } - proto._updateRender = function() { - // Render name link - renderNameLink(this); - }; -} + initRender() { + const config = this.config; -export function initRender(vm) { - const config = vm.config; + // Init markdown compiler + this.compiler = new Compiler(config, this.router); + if (inBrowser) { + /* eslint-disable-next-line camelcase */ + window.__current_docsify_compiler__ = this.compiler; + } - // 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 id = config.el || '#app'; - const navEl = dom.find('nav') || dom.create('nav'); + const el = dom.find(id); + let html = ''; + let navAppendToTarget = dom.body; - const el = dom.find(id); - let html = ''; - let navAppendToTarget = dom.body; + if (el) { + if (config.repo) { + html += tpl.corner(config.repo, config.cornerExternalLinkTarge); + } - if (el) { - if (config.repo) { - html += tpl.corner(config.repo, config.cornerExternalLinkTarge); - } + if (config.coverpage) { + html += tpl.cover(); + } - 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 (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); + } + } - if (!isBase64 && !isExternal && !isRelative) { - config.logo = getPath(vm.router.getBasePath(), config.logo); + html += tpl.main(config); + // Render main app + this._renderTo(el, html, true); + } else { + this.rendered = true; } - } - 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.mergeNavbar && isMobile) { + navAppendToTarget = dom.find('.sidebar'); + } else { + navEl.classList.add('app-nav'); - if (!config.repo) { - navEl.classList.add('no-badge'); - } - } + if (!config.repo) { + navEl.classList.add('no-badge'); + } + } - // Add nav - if (config.loadNavbar) { - dom.before(navAppendToTarget, navEl); - } + // 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); - } + 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'); + this._updateRender(); + dom.toggleClass(dom.body, 'ready'); + } + }; } diff --git a/src/core/router/history/hash.js b/src/core/router/history/hash.js index a2a52aee49..cf948683b2 100644 --- a/src/core/router/history/hash.js +++ b/src/core/router/history/hash.js @@ -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 */ diff --git a/src/core/router/index.js b/src/core/router/index.js index 923168898c..773f568958 100644 --- a/src/core/router/index.js +++ b/src/core/router/index.js @@ -4,44 +4,59 @@ 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); -} - -export function initRouter(vm) { - const config = vm.config; - const mode = config.routerMode || 'hash'; - let router; - - 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; +/** @typedef {import('../Docsify').Constructor} Constructor */ + +/** + * @template {!Constructor} T + * @param {T} Base - The class to extend + */ +export function Router(Base) { + return class Router extends Base { + updateRender() { + this.router.normalize(); + this.route = this.router.parse(); + dom.body.setAttribute('data-page', this.route.file); } - vm.$fetch(noop, vm.$resetEvents.bind(vm, params.source)); - lastRoute = vm.route; - }); + initRouter() { + this.route = {}; + + 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; + }); + } + }; }