diff --git a/src/core/config.js b/src/core/config.js new file mode 100644 index 00000000..d344aae9 --- /dev/null +++ b/src/core/config.js @@ -0,0 +1,38 @@ +import { merge, camelize, isPrimitive } from './util/core' + +const config = merge({ + el: '#app', + repo: '', + maxLevel: 6, + subMaxLevel: 0, + loadSidebar: null, + loadNavbar: null, + homepage: 'README.md', + coverpage: '', + basePath: '', + auto2top: false, + name: '', + themeColor: '', + nameLink: window.location.pathname, + ga: '' +}, window.$docsify) + +const script = document.currentScript || [].slice.call(document.getElementsByTagName('script')).pop() + +if (script) { + for (const prop in config) { + const val = script.getAttribute('data-' + camelize(prop)) + + if (isPrimitive(val)) { + config[prop] = val === '' ? true : val + } + } + + if (config.loadSidebar === true) config.loadSidebar = '_sidebar.md' + if (config.loadNavbar === true) config.loadNavbar = '_navbar.md' + if (config.coverpage === true) config.coverpage = '_coverpage.md' + if (config.repo === true) config.repo = '' + if (config.name === true) config.name = '' +} + +export default config diff --git a/src/core/event/index.js b/src/core/event/index.js new file mode 100644 index 00000000..46d9d284 --- /dev/null +++ b/src/core/event/index.js @@ -0,0 +1,11 @@ +export function eventMixin (Docsify) { + Docsify.prototype.$bindEvents = function () { + } + + Docsify.prototype.$resetEvents = function () { + } +} + +export function initEvent (vm) { + vm.$bindEvents() +} diff --git a/src/core/fetch/ajax.js b/src/core/fetch/ajax.js new file mode 100644 index 00000000..ba79b766 --- /dev/null +++ b/src/core/fetch/ajax.js @@ -0,0 +1,37 @@ +import progressbar from '../render/progressbar' +import { noop } from '../util/core' + +/** + * Simple ajax get + * @param {String} url + * @param {Boolean} [loading=false] has loading bar + * @return { then(resolve, reject), abort } + */ +export function get (url, hasLoading = false) { + const xhr = new XMLHttpRequest() + + xhr.open('GET', url) + xhr.send() + + return { + then: function (success, error = noop) { + const on = xhr.addEventListener + + if (hasLoading) { + const id = setInterval(_ => progressbar({}), 500) + + on('progress', progressbar) + on('loadend', evt => { + progressbar(evt) + clearInterval(id) + }) + } + + on('error', error) + on('load', ({ target }) => { + target.status >= 400 ? error(target) : success(target.response) + }) + }, + abort: () => xhr.readyState !== 4 && xhr.abort() + } +} diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js new file mode 100644 index 00000000..7771e898 --- /dev/null +++ b/src/core/fetch/index.js @@ -0,0 +1,13 @@ +import { callHook } from '../init/lifecycle' + +export function fetchMixin (Docsify) { + Docsify.prototype.$fetch = function () { + } +} + +export function initFetch (vm) { + vm.$fetch(result => { + vm.$resetEvents() + callHook(vm, 'doneEach') + }) +} diff --git a/src/core/index.js b/src/core/index.js new file mode 100644 index 00000000..96cab23a --- /dev/null +++ b/src/core/index.js @@ -0,0 +1,30 @@ +import { initMixin } from './init' +import { routeMixin } from './route' +import { renderMixin } from './render' +import { fetchMixin } from './fetch' +import { eventMixin } from './event' +import * as util from './util' +import { get as load } from './fetch/ajax' +import * as routeUtil from './route/util' + +function Docsify () { + this._init() +} + +initMixin(Docsify) +routeMixin(Docsify) +renderMixin(Docsify) +fetchMixin(Docsify) +eventMixin(Docsify) + +/** + * Global API + */ +window.Docsify = { + util: util.merge({ load }, util, routeUtil) +} + +/** + * Run Docsify + */ +setTimeout(() => new Docsify(), 0) diff --git a/src/core/init/index.js b/src/core/init/index.js new file mode 100644 index 00000000..8a6c2a3e --- /dev/null +++ b/src/core/init/index.js @@ -0,0 +1,27 @@ +import config from '../config' +import { initLifecycle, callHook } from './lifecycle' +import { initRender } from '../render' +import { initRoute } from '../route' +import { initEvent } from '../event' +import { initFetch } from '../fetch' +import { isFn } from '../util/core' + +export function initMixin (Docsify) { + Docsify.prototype._init = function () { + const vm = this + vm._config = config || {} + + initLifecycle(vm) // Init hooks + initPlugin(vm) // Install plugins + callHook(vm, 'init') + initRender(vm) // Render base DOM + initEvent(vm) // Bind events + initRoute(vm) // Add hashchange eventListener + initFetch(vm) // Fetch data + callHook(vm, 'ready') + } +} + +function initPlugin (vm) { + [].concat(vm.config.plugins).forEach(fn => isFn(fn) && fn(vm.bindHook)) +} diff --git a/src/core/init/lifecycle.js b/src/core/init/lifecycle.js new file mode 100644 index 00000000..e4a48a84 --- /dev/null +++ b/src/core/init/lifecycle.js @@ -0,0 +1,41 @@ +import { noop } from '../util/core' + +export function initLifecycle (vm) { + const hooks = ['init', 'beforeEach', 'afterEach', 'doneEach', 'ready'] + + vm._hooks = {} + vm.bindHook = {} + hooks.forEach(hook => { + const arr = vm._hooks[hook] = [] + vm._bindHook[hook] = fn => arr.push(fn) + }) +} + +export function callHook (vm, hook, data, next = noop) { + let newData = data + const queue = vm._hooks[hook] + + const step = function (index) { + const hook = queue[index] + if (index >= queue.length) { + next(newData) + } else { + if (typeof hook === 'function') { + if (hook.length === 2) { + hook(data, result => { + newData = result + step(index + 1) + }) + } else { + const result = hook(data) + newData = result !== undefined ? result : newData + step(index + 1) + } + } else { + step(index + 1) + } + } + } + + step(0) +} diff --git a/src/core/render/dom.js b/src/core/render/dom.js new file mode 100644 index 00000000..8277095c --- /dev/null +++ b/src/core/render/dom.js @@ -0,0 +1,12 @@ +const cacheNode = {} + +export function getCacheNode (el) { + if (typeof el === 'string') { + const selector = el + + el = cacheNode[el] || document.querySelector(el) + if (!el) console.error('Cannot find element:', selector) + } + + return el +} diff --git a/src/core/render/index.js b/src/core/render/index.js new file mode 100644 index 00000000..03ef3f0c --- /dev/null +++ b/src/core/render/index.js @@ -0,0 +1,9 @@ +export function renderMixin (Docsify) { + Docsify.prototype._renderTo = function (dom, content) { + + } +} + +export function initRender (vm) { + // init +} diff --git a/src/core/render/progressbar.js b/src/core/render/progressbar.js new file mode 100644 index 00000000..1da5b225 --- /dev/null +++ b/src/core/render/progressbar.js @@ -0,0 +1,45 @@ +import { isPrimitive } from '../util/core' + +let loadingEl +let timeId + +/** + * Init progress component + */ +function init () { + if (loadingEl) return + const div = document.createElement('div') + + div.classList.add('progress') + document.body.appendChild(div) + loadingEl = div +} +/** + * Render progress bar + */ +export default function ({ loaded, total, step }) { + let num + + loadingEl = init() + + if (!isPrimitive(step)) { + step = Math.floor(Math.random() * 5 + 1) + } + if (step) { + num = parseInt(loadingEl.style.width, 10) + step + num = num > 80 ? 80 : num + } else { + num = Math.floor(loaded / total * 100) + } + + loadingEl.style.opacity = 1 + loadingEl.style.width = num >= 95 ? '100%' : num + '%' + + if (num >= 95) { + clearTimeout(timeId) + timeId = setTimeout(_ => { + loadingEl.style.opacity = 0 + loadingEl.style.width = '0%' + }, 200) + } +} diff --git a/src/tpl.js b/src/core/render/tpl.js similarity index 98% rename from src/tpl.js rename to src/core/render/tpl.js index 66551296..df589791 100644 --- a/src/tpl.js +++ b/src/core/render/tpl.js @@ -1,4 +1,4 @@ -import { isMobile } from './util' +import { isMobile } from '../util/core' /** * Render github corner * @param {Object} data diff --git a/src/core/route/hash.js b/src/core/route/hash.js new file mode 100644 index 00000000..90d180c6 --- /dev/null +++ b/src/core/route/hash.js @@ -0,0 +1,24 @@ +import { cleanPath, getLocation } from './util' + +export function ensureSlash () { + const path = getHash() + if (path.charAt(0) === '/') return + replaceHash('/' + path) +} + +export function getHash () { + // We can't use window.location.hash here because it's not + // consistent across browsers - Firefox will pre-decode it! + const href = window.location.href + const index = href.indexOf('#') + return index === -1 ? '' : href.slice(index + 1) +} + +function replaceHash (path) { + const i = window.location.href.indexOf('#') + window.location.replace( + window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path + ) +} + +// TODO 把第二个 hash 转成 ?id= diff --git a/src/core/route/index.js b/src/core/route/index.js new file mode 100644 index 00000000..011db741 --- /dev/null +++ b/src/core/route/index.js @@ -0,0 +1,17 @@ +import { ensureSlash } from './hash' + +export function routeMixin (Docsify) { + Docsify.prototype.$route = { + query: location.query || {}, + path: location.path || '/', + base: '' + } +} + +export function initRoute (vm) { + ensureSlash() + window.addEventListener('hashchange', () => { + ensureSlash() + vm.$fetch() + }) +} diff --git a/src/core/route/util.js b/src/core/route/util.js new file mode 100644 index 00000000..4cbf898b --- /dev/null +++ b/src/core/route/util.js @@ -0,0 +1,11 @@ +export function cleanPath (path) { + return path.replace(/\/+/g, '/') +} + +export function getLocation (base) { + let path = window.location.pathname + if (base && path.indexOf(base) === 0) { + path = path.slice(base.length) + } + return (path || '/') + window.location.search + window.location.hash +} diff --git a/src/core/util/core.js b/src/core/util/core.js new file mode 100644 index 00000000..96889839 --- /dev/null +++ b/src/core/util/core.js @@ -0,0 +1,56 @@ +/** + * Create a cached version of a pure function. + */ +function cached (fn) { + const cache = Object.create(null) + return function cachedFn (str) { + const hit = cache[str] + return hit || (cache[str] = fn(str)) + } +} + +/** + * Camelize a hyphen-delimited string. + */ +const camelizeRE = /-(\w)/g +export const camelize = cached((str) => { + return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') +}) + +/** + * Simple Object.assign polyfill + */ +export const merge = Object.assign || function (to) { + const hasOwn = Object.prototype.hasOwnProperty + + for (let i = 1; i < arguments.length; i++) { + const from = Object(arguments[i]) + + for (const key in from) { + if (hasOwn.call(from, key)) { + to[key] = from[key] + } + } + } + + return to +} + +/** + * Check if value is primitive + */ +export function isPrimitive (value) { + return typeof value === 'string' || typeof value === 'number' +} + +/** + * Perform no operation. + */ +export function noop () {} + +/** + * Check if value is function + */ +export function isFn (obj) { + return typeof obj === 'function' +} diff --git a/src/core/util/env.js b/src/core/util/env.js new file mode 100644 index 00000000..1e79cfe0 --- /dev/null +++ b/src/core/util/env.js @@ -0,0 +1,5 @@ +export const UA = window.navigator.userAgent.toLowerCase() + +export const isIE = UA && /msie|trident/.test(UA) + +export const isMobile = document.body.clientWidth <= 600 diff --git a/src/core/util/index.js b/src/core/util/index.js new file mode 100644 index 00000000..bfcc8b27 --- /dev/null +++ b/src/core/util/index.js @@ -0,0 +1,2 @@ +export * from './core' +export * from './env' diff --git a/src/polyfill.js b/src/core/util/polyfill/css-vars.js similarity index 91% rename from src/polyfill.js rename to src/core/util/polyfill/css-vars.js index f3a032b7..f0200e6f 100644 --- a/src/polyfill.js +++ b/src/core/util/polyfill/css-vars.js @@ -1,4 +1,4 @@ -import { load } from './util' +import { get } from '../fetch/ajax' function replaceVar (block) { block.innerHTML = block.innerHTML.replace(/var\(\s*--theme-color.*?\)/g, $docsify.themeColor) @@ -18,7 +18,7 @@ export function cssVars () { if (!/\.css$/.test(href)) return - load(href).then(res => { + get(href).then(res => { const style = document.createElement('style') style.innerHTML = res diff --git a/src/hook.js b/src/hook.js deleted file mode 100644 index 401765ae..00000000 --- a/src/hook.js +++ /dev/null @@ -1,57 +0,0 @@ -export default class Hook { - constructor () { - this.beforeHooks = [] - this.afterHooks = [] - this.initHooks = [] - this.readyHooks = [] - this.doneEachHooks = [] - } - - beforeEach (fn) { - this.beforeHooks.push(fn) - } - - afterEach (fn) { - this.afterHooks.push(fn) - } - - doneEach (fn) { - this.doneEachHooks.push(fn) - } - - init (fn) { - this.initHooks.push(fn) - } - - ready (fn) { - this.readyHooks.push(fn) - } - - emit (name, data, next) { - let newData = data - const queue = this[name + 'Hooks'] - const step = function (index) { - const hook = queue[index] - if (index >= queue.length) { - next && next(newData) - } else { - if (typeof hook === 'function') { - if (hook.length === 2) { - hook(data, result => { - newData = result - step(index + 1) - }) - } else { - const result = hook(data) - newData = result !== undefined ? result : newData - step(index + 1) - } - } else { - step(index + 1) - } - } - } - - step(0) - } -} diff --git a/src/render.js b/src/render.js index 26a5d538..41250887 100644 --- a/src/render.js +++ b/src/render.js @@ -235,36 +235,3 @@ export function renderCover (content) { event.sticky() } - -/** - * render loading bar - * @return {[type]} [description] - */ -export function renderLoading ({ loaded, total, step }) { - let num - - if (!CACHE.loading) { - const div = document.createElement('div') - - div.classList.add('progress') - document.body.appendChild(div) - CACHE.loading = div - } - if (step) { - num = parseInt(CACHE.loading.style.width, 10) + step - num = num > 80 ? 80 : num - } else { - num = Math.floor(loaded / total * 100) - } - - CACHE.loading.style.opacity = 1 - CACHE.loading.style.width = num >= 95 ? '100%' : num + '%' - - if (num >= 95) { - clearTimeout(renderLoading.cacheTimeout) - renderLoading.cacheTimeout = setTimeout(_ => { - CACHE.loading.style.opacity = 0 - CACHE.loading.style.width = '0%' - }, 200) - } -}