diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9d08a1a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..adcdf4b --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,69 @@ +// http://eslint.org/docs/user-guide/configuring + +module.exports = { + root: true, + parserOptions: { + parser: 'babel-eslint', + sourceType: 'module' + }, + extends: [ + // https://github.com/vuejs/eslint-plugin-vue#bulb-rules + 'plugin:vue/essential', + 'plugin:vue/recommended', + 'plugin:vue/strongly-recommended', + // https://github.com/standard/standard/blob/master/docs/RULES-en.md + 'standard', + 'prettier/standard' + ], + // required to lint *.vue files + plugins: ['vue'], + // add your custom rules here + rules: { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + 'no-multi-spaces': ['error', { ignoreEOLComments: true }], + 'no-template-curly-in-string': 0, + // to many false positives + 'vue/no-side-effects-in-computed-properties': 0, + // fix unused var error for JSX custom tags + 'vue/jsx-uses-vars': 2, + 'vue/require-default-prop': 0, + 'vue/name-property-casing': ['error', 'kebab-case'], + 'vue/component-name-in-template-casing': ['error', 'kebab-case'], + 'vue/html-indent': [ + 'error', + 2, + { + attribute: 1, + baseIndent: 0, + closeBracket: 0, + alignAttributesVertically: true + } + ], + 'vue/html-self-closing': [ + 'error', + { + html: { + void: 'never', + normal: 'always', + component: 'always' + }, + svg: 'always', + math: 'always' + } + ], + 'vue/html-closing-bracket-spacing': [ + 'error', + { + startTag: 'never', + endTag: 'never', + selfClosingTag: 'never' + } + ], + 'vue/no-v-html': 0 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..726398f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +one/build/deps.json +.DS_Store +.nuxt +.vscode +pages +components/demos +dist +logs +assets/data +static/images/mermaid diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba1f8f6 --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +# veui/docs + +> VEUI 文档。 + +## 本地安装 + +`git clone` 到本地后,在项目根目录下运行: + +```shell +npm i +npm run dev +``` + +后在浏览访问 `http://localhost:3000` 即可。 + +## 文档编写 + +开发相关文档位于 `one/docs/development` 下。文档目录结构与网站的目录结构一致,新建 `.md` 文件后需要在 `one/docs/nav.json` 中新建相应的条目,作为目录配置。添加 `sub: true` 将缩进一个层级。 + +### 组件文档结构 + +每个组件的文档请按如下顺序编写: + +1. 示例 +2. API + + 1. 属性 + 2. 插槽 + 3. 作用域插槽 + 4. 事件 + 5. 方法 + +3. 全局配置 + + 1. `veui` 中的默认 + 2. `veui-theme-dls` 中的默认配置 + +4. 图标名称 + +另外,如有关联组件请在最开始进行说明。比如: + +```md +:::tip +`Select` 组件可以内联 [`Option`](./option) 或 [`OptionGroup`](./option-group) 组件使用。 +::: +``` + +### 在文档中插入示例 + +使用 Markdown 的 shortcode 语法,如下: + +```md +[[ demo src="../demo/button.vue"]] +``` + +路径为 demo 文件相对于当前文档文件的路径。Demo 文件是一个 Vue 单文件组件,最后会将代码展示到文档中。可以编写多个 ` diff --git a/components/OneDemo.vue b/components/OneDemo.vue new file mode 100644 index 0000000..c4300ea --- /dev/null +++ b/components/OneDemo.vue @@ -0,0 +1,137 @@ + + + + + + + diff --git a/components/OneDetails.vue b/components/OneDetails.vue new file mode 100644 index 0000000..7b57eea --- /dev/null +++ b/components/OneDetails.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/components/OneFooter.vue b/components/OneFooter.vue new file mode 100644 index 0000000..e8614e5 --- /dev/null +++ b/components/OneFooter.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/components/OneHeader.vue b/components/OneHeader.vue new file mode 100644 index 0000000..2e9373b --- /dev/null +++ b/components/OneHeader.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/components/OneMenu.vue b/components/OneMenu.vue new file mode 100644 index 0000000..5941931 --- /dev/null +++ b/components/OneMenu.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/layouts/default.vue b/layouts/default.vue new file mode 100644 index 0000000..b20dae0 --- /dev/null +++ b/layouts/default.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/middleware/README.md b/middleware/README.md new file mode 100644 index 0000000..edb9129 --- /dev/null +++ b/middleware/README.md @@ -0,0 +1,9 @@ +# MIDDLEWARE + +This directory contains your Application Middleware. +The middleware lets you define custom function to be ran before rendering a page or a group of pages (layouts). + +More information about the usage of this directory in the documentation: +https://nuxtjs.org/guide/routing#middleware + +**This directory is not required, you can delete it if you don't want to use it.** diff --git a/nuxt.config.js b/nuxt.config.js new file mode 100644 index 0000000..bc5a738 --- /dev/null +++ b/nuxt.config.js @@ -0,0 +1,125 @@ +const path = require('path') + +function resolve (p) { + return path.resolve(__dirname, p) +} + +function appendLoader (config, loader) { + config.module.rules.push(loader) +} + +module.exports = { + target: 'static', + + /** + * Headers of the page + */ + head: { + title: 'VEUI', + meta: [ + { charset: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { + hid: 'description', + name: 'description', + content: 'Website for VEUI: an enterprise component library for Vue.js.' + } + ], + link: [ + { rel: 'shortcut icon', href: 'https://www.baidu.com/favicon.ico' }, + { + rel: 'stylesheet', + href: + 'https://code.bdstatic.com/npm/docsearch.js@2.6.3/dist/cdn/docsearch.min.css' + } + ], + script: [ + { + src: + 'https://code.bdstatic.com/npm/docsearch.js@2.6.3/dist/cdn/docsearch.min.js', + body: true + } + ] + }, + /** + * Customize the progress bar color + */ + loading: { color: '#1e1f24' }, + + pageTransition: { + css: false + }, + + css: ['veui-theme-dls/common.less', '@/assets/styles/global.styl'], + + plugins: [ + { src: '~plugins/i18n.js' }, + { src: '~plugins/l10n.js' }, + { src: '~plugins/hm.js', ssr: false }, + { src: '~plugins/algolia.js', ssr: false } + ], + + generate: { + subFolders: false + }, + + /** + * Build configuration + */ + build: { + transpile: ['veui', 'vue-awesome', 'resize-detector', 'less-plugin-dls'], + + babel: { + plugins: ['veui', 'lodash'] + }, + + loaders: { + vue: { + compilerOptions: { + whitespace: 'condense' + } + }, + stylus: { + 'include css': true + }, + less: { + javascriptEnabled: true + } + }, + + extend (config) { + /** + * veui-loader + */ + appendLoader(config, { + enforce: 'pre', + test: /\.vue$/, + loader: 'veui-loader', + options: { + locale: ['zh-Hans', 'en-US'], + modules: [ + { + package: 'veui-theme-dls', + fileName: '{module}.less' + }, + { + package: 'veui-theme-dls', + fileName: '{module}.js', + transform: false + } + ] + }, + include: [resolve('pages'), resolve('node_modules/veui')] + }) + + appendLoader(config, { + test: /\.js$/, + include: [resolve('node_modules/focus-visible')], + loader: 'short-circuit-loader', + options: { + expr: "process.env.VUE_ENV === 'server'" + } + }) + } + } +} diff --git a/one/build/customBlock.js b/one/build/customBlock.js new file mode 100644 index 0000000..b467b75 --- /dev/null +++ b/one/build/customBlock.js @@ -0,0 +1,226 @@ +'use strict' + +var trim = require('trim-trailing-lines') + +module.exports = customBlock + +var C_NEWLINE = '\n' +var C_TAB = '\t' +var C_SPACE = ' ' +var C_COLON = ':' + +var MIN_FENCE_COUNT = 3 +var CODE_INDENT_COUNT = 4 + +function customBlock (eat, value, silent) { + var self = this + var length = value.length + 1 + var index = 0 + var subvalue = '' + var fenceCount + var marker + var character + var flag + var queue + var content + var exdentedContent + var closing + var exdentedClosing + var indent + var now + + /* Eat initial spacing. */ + while (index < length) { + character = value.charAt(index) + + if (character !== C_SPACE && character !== C_TAB) { + break + } + + subvalue += character + index++ + } + + indent = index + + /* Eat the fence. */ + character = value.charAt(index) + + if (character !== C_COLON) { + return + } + + index++ + marker = character + fenceCount = 1 + subvalue += character + + while (index < length) { + character = value.charAt(index) + + if (character !== marker) { + break + } + + subvalue += character + fenceCount++ + index++ + } + + if (fenceCount < MIN_FENCE_COUNT) { + return + } + + /* Eat spacing before flag. */ + while (index < length) { + character = value.charAt(index) + + if (character !== C_SPACE && character !== C_TAB) { + break + } + + subvalue += character + index++ + } + + /* Eat flag. */ + flag = '' + queue = '' + + while (index < length) { + character = value.charAt(index) + + if (character === C_NEWLINE || character === C_COLON) { + break + } + + if (character === C_SPACE || character === C_TAB) { + queue += character + } else { + flag += queue + character + queue = '' + } + + index++ + } + + character = value.charAt(index) + + if (character && character !== C_NEWLINE) { + return + } + + if (silent) { + return true + } + + now = eat.now() + now.column += subvalue.length + now.offset += subvalue.length + + subvalue += flag + flag = self.decode.raw(self.unescape(flag), now) + + if (queue) { + subvalue += queue + } + + queue = '' + closing = '' + exdentedClosing = '' + content = '' + exdentedContent = '' + + /* Eat content. */ + while (index < length) { + character = value.charAt(index) + content += closing + exdentedContent += exdentedClosing + closing = '' + exdentedClosing = '' + + if (character !== C_NEWLINE) { + content += character + exdentedClosing += character + index++ + continue + } + + /* Add the newline to `subvalue` if its the first + * character. Otherwise, add it to the `closing` + * queue. */ + if (content) { + closing += character + exdentedClosing += character + } else { + subvalue += character + } + + queue = '' + index++ + + while (index < length) { + character = value.charAt(index) + + if (character !== C_SPACE) { + break + } + + queue += character + index++ + } + + closing += queue + exdentedClosing += queue.slice(indent) + + if (queue.length >= CODE_INDENT_COUNT) { + continue + } + + queue = '' + + while (index < length) { + character = value.charAt(index) + + if (character !== marker) { + break + } + + queue += character + index++ + } + + closing += queue + exdentedClosing += queue + + if (queue.length < fenceCount) { + continue + } + + queue = '' + + while (index < length) { + character = value.charAt(index) + + if (character !== C_SPACE && character !== C_TAB) { + break + } + + closing += character + exdentedClosing += character + index++ + } + + if (!character || character === C_NEWLINE) { + break + } + } + + subvalue += content + closing + + return eat(subvalue)({ + type: 'customblock', + className: (flag || '').trim().replace(/\s+/g, ' '), + value: trim(exdentedContent) + }) +} diff --git a/one/build/deps.js b/one/build/deps.js new file mode 100644 index 0000000..0dfd160 --- /dev/null +++ b/one/build/deps.js @@ -0,0 +1,48 @@ +import path from 'path' +import { readFileSync, writeFileSync } from './util' +import { merge } from 'lodash' + +function load () { + try { + return JSON.parse(readFileSync(resolve('./deps.json'))) + } catch (e) { + return {} + } +} + +function save (deps) { + writeFileSync(resolve('./deps.json'), JSON.stringify(deps, null, ' ')) +} + +function resolve (file) { + return path.resolve(__dirname, file) +} + +export function get (file) { + let deps = load() + return deps[file] +} + +export function add (data) { + let deps = load() + save(merge(deps, data)) +} + +export function remove (data) { + let deps = load() + Object.keys(data).forEach(key => { + if (deps[key]) { + delete deps[key][data[key]] + } + }) + save(deps) +} + +export function removeFile (file) { + let deps = load() + delete deps[file] + + Object.keys(deps).forEach(key => { + delete deps[key][file] + }) +} diff --git a/one/build/details.js b/one/build/details.js new file mode 100644 index 0000000..841b6b2 --- /dev/null +++ b/one/build/details.js @@ -0,0 +1,226 @@ +'use strict' + +var trim = require('trim-trailing-lines') + +module.exports = details + +var C_NEWLINE = '\n' +var C_TAB = '\t' +var C_SPACE = ' ' +var C_PLUS = '+' + +var MIN_FENCE_COUNT = 3 +var CODE_INDENT_COUNT = 4 + +function details (eat, value, silent) { + var self = this + var length = value.length + 1 + var index = 0 + var subvalue = '' + var fenceCount + var marker + var character + var flag + var queue + var content + var exdentedContent + var closing + var exdentedClosing + var indent + var now + + /* Eat initial spacing. */ + while (index < length) { + character = value.charAt(index) + + if (character !== C_SPACE && character !== C_TAB) { + break + } + + subvalue += character + index++ + } + + indent = index + + /* Eat the fence. */ + character = value.charAt(index) + + if (character !== C_PLUS) { + return + } + + index++ + marker = character + fenceCount = 1 + subvalue += character + + while (index < length) { + character = value.charAt(index) + + if (character !== marker) { + break + } + + subvalue += character + fenceCount++ + index++ + } + + if (fenceCount < MIN_FENCE_COUNT) { + return + } + + /* Eat spacing before flag. */ + while (index < length) { + character = value.charAt(index) + + if (character !== C_SPACE && character !== C_TAB) { + break + } + + subvalue += character + index++ + } + + /* Eat flag. */ + flag = '' + queue = '' + + while (index < length) { + character = value.charAt(index) + + if (character === C_NEWLINE || character === C_PLUS) { + break + } + + if (character === C_SPACE || character === C_TAB) { + queue += character + } else { + flag += queue + character + queue = '' + } + + index++ + } + + character = value.charAt(index) + + if (character && character !== C_NEWLINE) { + return + } + + if (silent) { + return true + } + + now = eat.now() + now.column += subvalue.length + now.offset += subvalue.length + + subvalue += flag + flag = self.decode.raw(self.unescape(flag), now) + + if (queue) { + subvalue += queue + } + + queue = '' + closing = '' + exdentedClosing = '' + content = '' + exdentedContent = '' + + /* Eat content. */ + while (index < length) { + character = value.charAt(index) + content += closing + exdentedContent += exdentedClosing + closing = '' + exdentedClosing = '' + + if (character !== C_NEWLINE) { + content += character + exdentedClosing += character + index++ + continue + } + + /* Add the newline to `subvalue` if its the first + * character. Otherwise, add it to the `closing` + * queue. */ + if (content) { + closing += character + exdentedClosing += character + } else { + subvalue += character + } + + queue = '' + index++ + + while (index < length) { + character = value.charAt(index) + + if (character !== C_SPACE) { + break + } + + queue += character + index++ + } + + closing += queue + exdentedClosing += queue.slice(indent) + + if (queue.length >= CODE_INDENT_COUNT) { + continue + } + + queue = '' + + while (index < length) { + character = value.charAt(index) + + if (character !== marker) { + break + } + + queue += character + index++ + } + + closing += queue + exdentedClosing += queue + + if (queue.length < fenceCount) { + continue + } + + queue = '' + + while (index < length) { + character = value.charAt(index) + + if (character !== C_SPACE && character !== C_TAB) { + break + } + + closing += character + exdentedClosing += character + index++ + } + + if (!character || character === C_NEWLINE) { + break + } + } + + subvalue += content + closing + + return eat(subvalue)({ + type: 'details', + summary: flag || null, + value: trim(exdentedContent) + }) +} diff --git a/one/build/generate.js b/one/build/generate.js new file mode 100644 index 0000000..5f72c8c --- /dev/null +++ b/one/build/generate.js @@ -0,0 +1,3 @@ +import { generatePages } from './generator' + +generatePages() diff --git a/one/build/generator.js b/one/build/generator.js new file mode 100644 index 0000000..c76abd3 --- /dev/null +++ b/one/build/generator.js @@ -0,0 +1,105 @@ +import { statSync } from 'fs' +import { resolve, relative, extname, basename, sep } from 'path' +import readdirpSync from 'recursive-readdir-sync' +import rimraf from 'rimraf' +import { copyFileSync, replaceExtSync } from './util' +import { renderDocToPage } from './page' +import { get, removeFile } from './deps' + +const DOCS_DIR = resolve(__dirname, '../docs') +const PAGES_DIR = resolve(__dirname, '../../pages') +const DEMOS_DIR = resolve(__dirname, '../../components/demos') +const MERMAID_DIR = resolve(__dirname, '../../static/images/mermaid') +const ASSETS_DIR = resolve(__dirname, '../../assets') + +export function generatePages (file, stats) { + if (!file) { + rimraf.sync(PAGES_DIR) + rimraf.sync(DEMOS_DIR) + rimraf.sync(MERMAID_DIR) + rimraf.sync(resolve(__dirname, './deps.json')) + console.log('Regenerating all files...') + handleFile(DOCS_DIR) + console.log('...done.') + } else { + handleFile(file, stats) + } +} + +function handleFile (file, stats) { + let segments = relative(DOCS_DIR, file).split(sep) + if (segments.some(segment => { + return segment.startsWith('_') || segment.startsWith('.') + })) { + return + } + + let remove = stats ? stats.remove : false + let dir = stats ? stats.dir : statSync(file).isDirectory() + if (dir) { + if (remove) { + rimraf.sync(file) + return + } + + let children = readdirpSync(file) + children.forEach(child => { + handleFile(child, remove) + }) + return + } + + let ext = extname(file).toLowerCase() + + /* eslint-disable indent */ + /* There seems to be something wrong with FECS here */ + switch (ext) { + case '.md': { + if (remove) { + let relDest = replaceExtSync(relative(DOCS_DIR, file), 'vue') + rimraf.sync(resolve(PAGES_DIR, relDest)) + console.log(`[${relDest}] removed.`) + } else { + let dest = relative(DOCS_DIR, file) + renderDocToPage(dest) + console.log(`[${dest}] synced.`) + } + break + } + case '.json': { + if (basename(file) === 'nav.json') { + copyFileSync(file, resolve(ASSETS_DIR, 'data', 'nav.json')) + console.log('[nav.json] synced.') + } + break + } + default: { + let relDest = relative(DOCS_DIR, file) + + let dest = relDest.split(sep).indexOf('demo') === -1 + ? resolve(PAGES_DIR, relDest) + : resolve(DEMOS_DIR, relDest) + if (remove) { + rimraf.sync(dest) + console.log(`[${relDest}] removed.`) + } else { + copyFileSync(file, dest) + console.log(`[${relDest}] synced.`) + } + break + } + } + /* eslint-enable indent */ + + if (remove) { + removeFile(file) + return + } + + let deps = get(file) + if (deps) { + Object.keys(deps).forEach(dep => { + handleFile(dep) + }) + } +} diff --git a/one/build/i18n.js b/one/build/i18n.js new file mode 100644 index 0000000..8898427 --- /dev/null +++ b/one/build/i18n.js @@ -0,0 +1,13 @@ +export const DEFAULT_LOCALE = 'zh-Hans' +export const LOCALES = [ + { + code: DEFAULT_LOCALE, + label: '简体中文' + }, + { + code: 'en-US', + label: 'English (US)' + } +] +export const LOCALE_CODES = LOCALES.map(l => l.code) +export const RE_LOCALE = new RegExp(`^\\/(${LOCALE_CODES.join('|')})\\/`) diff --git a/one/build/language.js b/one/build/language.js new file mode 100644 index 0000000..dd70f26 --- /dev/null +++ b/one/build/language.js @@ -0,0 +1,99 @@ +/* eslint-disable fecs-camelcase */ +/* eslint-disable babel/new-cap */ +export function vue (hljs) { + const XML_IDENT_RE = '[A-Za-z0-9\\._:-]+' + const TAG_INTERNALS = { + endsWithParent: true, + illegal: /`]+/} + ] + } + ] + } + ] + } + return { + case_insensitive: true, + contains: [ + hljs.COMMENT( + '', + { + relevance: 10 + } + ), + { + className: 'tag', + /* + The lookahead pattern (?=...) ensures that 'begin' only matches + '|$)', + end: '>', + keywords: {name: 'style'}, + contains: [TAG_INTERNALS], + starts: { + end: '', + returnEnd: true, + subLanguage: ['css', 'less', 'scss', 'stylus'] + } + }, + { + className: 'tag', + // See the comment in the ` + ) + }) + return content.join('\n\n') +} + +function stringifyAttrs (attrs) { + let result = Object.keys(attrs) + .map(key => { + let val = attrs[key] + if (typeof val !== 'boolean') { + return `${escape(key)}="${escape(val)}"` + } + return val ? `${escape(key)}` : '' + }) + .join(' ') + + return result ? ` ${result}` : '' +} diff --git a/one/build/remark-details.js b/one/build/remark-details.js new file mode 100644 index 0000000..aaa086c --- /dev/null +++ b/one/build/remark-details.js @@ -0,0 +1,39 @@ +import tokenizer from './details' +import visit from 'unist-util-visit' +import { escape } from 'lodash' +import { render } from './page' + +const NAME = 'details' + +export default function attacher () { + let proto = this.Parser.prototype + + proto.blockTokenizers[NAME] = tokenizer + proto.interruptParagraph.push([NAME]) + proto.interruptList.push([NAME]) + proto.interruptBlockquote.push([NAME]) + + let methods = proto.blockMethods + methods.unshift(NAME) + + return (tree, file) => { + let { path, data } = file + + if (!data) { + file.data = data = {} + } + if (!data.components) { + data.components = {} + } + + visit(tree, NAME, ({ summary, value }, index, parent) => { + data.components['OneDetails'] = true + + let { contents } = render(value, path, data) + parent.children.splice(index, 1, { + type: 'html', + value: `${contents}` + }) + }) + } +} diff --git a/one/build/remark-extract-frontmatter.js b/one/build/remark-extract-frontmatter.js new file mode 100644 index 0000000..50f18a0 --- /dev/null +++ b/one/build/remark-extract-frontmatter.js @@ -0,0 +1,16 @@ +import visit from 'unist-util-visit' +import remove from 'unist-util-remove' +import yaml from 'js-yaml' + +export default function attacher () { + return (tree, file) => { + let { data } = file + + visit(tree, 'yaml', node => { + data.meta = yaml.safeLoad(node.value) + return visit.EXIT + }) + + remove(tree, ['yaml', 'toml']) + } +} diff --git a/one/build/remark-ref.js b/one/build/remark-ref.js new file mode 100644 index 0000000..b22167f --- /dev/null +++ b/one/build/remark-ref.js @@ -0,0 +1,49 @@ +import tokenizer from './refBlock' +import visit from 'unist-util-visit' +import remove from 'unist-util-remove' +import { render } from './page' + +const NAME = 'refblock' +const RE_REF = /^\^([-_a-z0-9]+)/i + +export default function attacher () { + let proto = this.Parser.prototype + + proto.blockTokenizers[NAME] = tokenizer + proto.interruptParagraph.push([NAME]) + proto.interruptList.push([NAME]) + proto.interruptBlockquote.push([NAME]) + + let methods = proto.blockMethods + methods.unshift(NAME) + + return (tree, file) => { + let { path, data } = file + + if (!data) { + file.data = data = {} + } + if (!data.refs) { + data.refs = {} + } + + visit(tree, NAME, ({ id, value }) => { + let { contents } = render(value, path, data) + data.refs[id] = contents + }) + remove(tree, NAME) + + visit(tree, 'linkReference', (node, index, parent) => { + let { identifier } = node + let [match, id] = identifier.match(RE_REF) + if (!match || !id || !data.refs[id]) { + return + } + + parent.children.splice(index, 1, { + type: 'html', + value: data.refs[id] + }) + }) + } +} diff --git a/one/build/util.js b/one/build/util.js new file mode 100644 index 0000000..19c6df2 --- /dev/null +++ b/one/build/util.js @@ -0,0 +1,29 @@ +import { readFileSync as fsReadFileSync, writeFileSync as fsWriteFileSync } from 'fs' +import { dirname } from 'path' +import mkdirp from 'mkdirp' +import crypto from 'crypto' + +export function readFileSync (file) { + return fsReadFileSync(file, 'utf8') +} + +export function writeFileSync (file, content) { + mkdirp.sync(dirname(file)) + fsWriteFileSync(file, content, 'utf8') +} + +export function copyFileSync (src, dest) { + mkdirp.sync(dirname(dest)) + writeFileSync(dest, readFileSync(src)) +} + +const RE_EXT = /\.([^.]+)$/ +export function replaceExtSync (file, ext) { + return file.replace(RE_EXT, `.${ext}`) +} + +export function hash (path) { + let hash = crypto.createHash('sha1') + hash.update(path) + return hash.digest('hex').substring(0, 7) +} diff --git a/one/build/watch.js b/one/build/watch.js new file mode 100644 index 0000000..b4d828b --- /dev/null +++ b/one/build/watch.js @@ -0,0 +1,33 @@ +import { resolve } from 'path' +import { debounce } from 'lodash' +import chokidar from 'chokidar' +import { generatePages } from './generator' + +let watcher = chokidar.watch(resolve(__dirname, '../docs'), { + ignoreInitial: true +}) + +watcher + .on('all', debounce(generate, 1000)) + +function generate (type, path) { + switch (type) { + case 'add': + generatePages(path, { dir: false, remove: false }) + break + case 'addDir': + generatePages(path, { dir: true, remove: false }) + break + case 'change': + generatePages(path) + break + case 'unlink': + generatePages(path, { dir: false, remove: true }) + break + case 'unlinkDir': + generatePages(path, { dir: true, remove: true }) + break + default: + break + } +} diff --git a/one/docs/advanced/custom-rules.md b/one/docs/advanced/custom-rules.md new file mode 100644 index 0000000..897a1ae --- /dev/null +++ b/one/docs/advanced/custom-rules.md @@ -0,0 +1,89 @@ +# 自定义校验规则 + +对于多值校验,[表单 › validators 属性](../components/form#属性)提供了比较完善的功能来实现自定义校验。对于单值校验,`Field` 组件内置了 7 种常见规则,具体参考[表单项 › rule 属性](../components/field#属性)。如果无法覆盖需求,`VEUI` 校验规则模块允许你添加自定义规则。 + +## 示例 + +```js +import ruleManager from 'veui/manager/rule' +ruleManager.addRule('range', { + validate (value, ruleValue) { + // 仅实现大小校验部分 + let range = value.split('-') + return +range[0] >= ruleValue.floor && +range[1] <= ruleValue.ceil + }, + message: '范围值必须在限定区间内', + priority: 100 +}) +``` + +```html +... +``` + +## API + +| 名称 | 类型 | 描述 | +| -- | -- | -- | +| `validate` | `function(value: *, ruleValue: ?*=)` | 校验逻辑,`value` 为 `Field` 需要校验的值,`ruleValue` 可选,根据规则需要添加,表示规则的限定值。 | +| `message` | `function|string` | [^message] | +| `priority` | `number` | [^priority] | + +^^^message +默认出错信息。 + +若类型为 `string`,可以通过 `{ruleValue}` 引用 `ruleValue`、`{value}` 引用 `value`。例如: + +```js +let minLengthRule = { + validate (value, ruleValue) { + return !isEmpty(value) ? val.length >= ruleValue : true + }, + message: '字符长度不能短于 {ruleValue},当前长度 {value}', + priority: 100 +} +``` + +若类型为 `function`,参数为 `(ruleValue: ?*=, value: *)`。例如: + +```js +let minLengthRule = { + validate (value, ruleValue) { + return !isEmpty(value) ? val.length >= ruleValue : true + }, + message (ruleValue, value) { + return `字符长度不能短于${ruleValue},当前长度${value}` + }, + priority: 100 +} +``` + +:::tip +如果需要支持运行时切换语言,`message` 必须使用 `function` 类型。 +::: +^^^ + +^^^priority +规则优先级。数值低优先级高。 + ++++目前内置的优先级 +| 名称 | 优先级 | +| -- | -- | -- | +| `required` | `0` | +| `numeric` | `10` | +| `pattern` | `50` | +| `maxLength` | `100` | +| `minLength` | `100` | +| `max` | `200` | +| `min` | `200` | ++++ diff --git a/one/docs/advanced/global-config.md b/one/docs/advanced/global-config.md new file mode 100644 index 0000000..3aa538f --- /dev/null +++ b/one/docs/advanced/global-config.md @@ -0,0 +1,61 @@ +# 全局配置 + +VEUI 中很多组件都定义了全局配置项,允许开发者在使用时全局配置某个组件的行为细节。 + +例如,`Uploader` 组件可以统一配置上传模式,用户可以根据项目前后端接口、需要支持浏览器版本的具体情况选择使用 iframe 回调方式还是 XHR2 方式传递数据,也可以统一配置远端数据格式的转换函数。 + +VEUI 全局配置项可以通过 `veui/managers/config` 模块进行覆盖: + +```js +import config from 'veui/managers/config' + +config.set('uploader.requestMode', 'iframe') +config.set('uploader.convertResponse', ({ code, error, result }) => { + /** + * Transform from + * + * { + * code: 0, + * error: '...', + * result: { + * url: '...' + * } + * } + * + * to + * + * { + * success: true, + * message: '...', + * src: '...' + * } + */ + return { + success: code === 0, + src: error ? null : result.url, + message: error || null + } +}) +``` + +如果需要一次修改同一个组件的多项设置,可以使用如下写法: + +```js +import config from 'veui/managers/config' + +config.set({ + requestMode: 'iframe', + convertResponse: data => data +}, 'uploader') +``` + +`config.set()` 方法参数可为如下形式: + +* `(key: string, value: *, namespace: string)` +* `(values: Object<{key: string, value: *}>, namespace: string)` + +当提供了 `namespace` 参数时,最终生成的配置项键名为 \`${namespace}.${key}\`。 + +除此以外,还提供了相同参数列表的 `config.defaults()` 方法,区别在于当需要在配置项中写入的键值已经存在,则不会覆盖。 + +每个组件、指令等支持的全局配置请查看对应组件、指令的详情页。 diff --git a/one/docs/advanced/overlay.md b/one/docs/advanced/overlay.md new file mode 100644 index 0000000..ca6161c --- /dev/null +++ b/one/docs/advanced/overlay.md @@ -0,0 +1,124 @@ +# 浮层管理 + +在 VEUI 中,有大量组件使用到了浮层功能: + +* 各种类型的弹框:[对话框](../components/dialog)、[警告弹框](../components/alert-box)等; +* [下拉选择](../components/select); +* …… + +针对这些组件,我们抽离了具备如下功能的浮层模块: + +* 能够浮于页面上所有普通元素之上; +* 能够进行层叠顺序管理; +* 能够基于指定元素定位。 + +## 层叠覆盖 + +为了避免浮层被上层 `overflow: hidden` 的元素意外遮盖,我们将浮层根元素直接置于 `` 下统一管理。 + +在[浮层组件](../components/overlay)中,`.veui-overlay-box` 对应了浮层根元素,该元素在组件初始化的时候,会被放置到 `` 之下,组件销毁的时候,会被移除掉。 + +## 层叠顺序管理 + +在将浮层根元素置于 `` 下后,原有的层级嵌套关系会丢失,同时也无法通过原生的层叠上下文机制来控制浮层的层叠顺序。比如: + +* 某个对话框组件 A 上有一个下拉选择组件 B,那么 B 组件浮层应该位于 A 组件浮层之上。 +* 警告框浮层应该位于普通对话框浮层之上。 + +基于上述限制,浮层模块实现了自己的层叠顺序管理机制。整个浮层层级嵌套关系,是通过一棵树来表达的: + + + +树中每一个蓝色节点都对应关联到具体的[浮层组件](../componets/overlay)实例。针对上图,树的构造顺序可以是: + +1. 弹出“对话框 1”,创建一个“对话框 1”节点,根据节点权重信息创建一个分组,然后将分组挂在 root 节点之下。 +2. 弹出“对话框 2”,创建一个“对话框 2”节点,发现已经存在相同权重的分组,就直接将“对话框 2”节点放置在该分组的末尾位置。 +3. 在“对话框 2”中实例化一个“下拉选择 1”组件实例,由于“对话框 2”组件实例是“下拉选择 1”组件实例的父级,因此对应的浮层节点也应当具备父子关系,因此按照类似于“步骤 1”的顺序在“对话框 2”节点下生成分组及“下拉选择 1”节点。 +4. 此时由于程序运行出现了故障,弹出了“警告弹框 1”,由于“警告弹框”类型的组件相对于“对话框”组件具备更高的层级权重,因此在 root 之下新建了一个靠右的分组,并将生成的“警告弹框 1”节点置于分组末尾。 + +有了树之后,就会按照深度优先的遍历顺序生成每个节点的 `z-index` 值。 + +其中,基准 `z-index` 值可以通过全局配置对象进行配置: + +```js +import config from 'veui/managers/config' + +config.set('overlay.baseZIndex', 200) +``` + +:::warning +必须在[浮层组件](../components/overlay)引入之前设置基准 `z-index`,不然不会生效。 +::: + +可以针对组件类型,甚至组件实例粒度设置层叠优先级,层叠优先级值越大,最终生成的 `z-index` 值就越大。具有相同层叠优先级的同级组件实例,越靠后实例化的组件,生成的 `z-index` 值越大。 + +浮层组件、对话框组件、弹框组件等提供了 `priority` 属性,用于自定义组件实例的层叠优先级: + +```html + +``` + +一些比较特殊的组件,会提供基于组件类型粒度的层叠优先级配置: + +| 组件 | 配置字段 | 默认值 | 修改配置示例 | +| -- | -- | -- | -- | +| 警告弹框 | `alertbox.priority` | `100` | [^alert-box] | +| 确认弹框 | `confirmbox.priority` | `100` | [^confirm-box] | +| 输入弹框 | `promptbox.priority` | `100` | [^prompt-box] | + +^^^alert-box +```js +import config from 'veui/managers/config' + +config.set('alertbox.priority', 100) +``` +^^^ + +^^^confirm-box +```js +import config from 'veui/managers/config' + +config.set('confirmbox.priority', 100) +``` +^^^ + +^^^prompt-box +```js +import config from 'veui/managers/config' + +config.set('promptbox.priority', 100) +``` +^^^ + +总结起来,确定某个浮层系组件实例的层叠优先级的逻辑流程为: + +* 如果能够设置组件实例级别的层叠优先级,并且设置了,那么就使用这个层叠优先级值,否则进入下一步; +* 如果能够设置组件类型级别的层叠优先级,并且设置了,那么就使用这个层叠优先级值,否则进入下一步; +* 使用默认的层叠优先级值:`1`。 + +## 定位 + +VEUI 中,浮层支持两种定位方式: + +* 在页面范围内,以坐标值的形式进行定位; +* 相对于某个元素,指定偏移和变换规则进行定位。 + +以坐标方式定位时,需要自己写 CSS 进行控制(浮层模块内部只会生成浮层根元素的 `z-index` 值)。 + +相对元素定位时,可以通过[浮层组件](../components/overlay)的 `options` 属性描述偏移和变换规则。由于目前内部采用 [Tether](http://tether.io/) 实现,因此完整的配置项可以参考 [Tether 官网](http://tether.io/#options)。同时,也支持一些常见场景的简化配置:{ position: `${side}-${align}` },`side` 表示浮层根元素位于目标元素哪一边(`top`/`right`/`bottom`/`left`),`align` 表示对齐方式(`start`/`end`)。其中 `side` 是必须的,`align` 不传表示居中。推荐尽量使用简化的配置。 + +## 样式 + +由于浮层根元素被手动放置到 `` 元素之下了,要设置浮层内容的样式,就需要给浮层根元素指定 `class`。所有浮层系组件都支持 `overlay-class` 属性,通过该属性为浮层根元素设置 `class`: + +```vue +