├── .nvmrc
├── .husky
├── .gitignore
└── pre-commit
├── .npmrc
├── scripts
├── build.types.js
├── clean.js
├── build.document.js
├── env
│ ├── development.js
│ └── production.js
├── release.js
├── build.code.js
├── jest
│ ├── raw-loader.js
│ └── setup.js
├── webpack
│ └── loaders
│ │ └── markdown-loader
│ │ ├── extend-html.js
│ │ ├── highlight.js
│ │ ├── index.js
│ │ ├── md-section.js
│ │ ├── md-container.js
│ │ └── markdown-it.js
├── types.json
├── dev.js
├── example.js
└── args.js
├── .gitignore
├── packages
├── core
│ ├── util
│ │ ├── index.ts
│ │ ├── component.ts
│ │ └── dom.ts
│ ├── assets
│ │ ├── img
│ │ │ └── xform-tip.png
│ │ └── css
│ │ │ ├── variables.css
│ │ │ └── common.css
│ ├── README.md
│ ├── api
│ │ ├── Exports.ts
│ │ ├── test
│ │ │ ├── Preset.spec.ts
│ │ │ └── Config.spec.ts
│ │ ├── Slots.ts
│ │ ├── Preset.ts
│ │ ├── Config.ts
│ │ ├── Store.ts
│ │ ├── DefaultValue.ts
│ │ └── Logic
│ │ │ ├── logic.ts
│ │ │ └── builtin.tsx
│ ├── component
│ │ ├── FormBuilder
│ │ │ └── component.css
│ │ ├── FormViewer
│ │ │ └── component.css
│ │ ├── FormDesigner
│ │ │ └── component.module.css
│ │ └── FormItem
│ │ │ └── component.css
│ ├── index.css
│ ├── model
│ │ ├── index.ts
│ │ ├── Exports.ts
│ │ ├── Serializable.ts
│ │ ├── action.ts
│ │ ├── Emitter.ts
│ │ └── Button.ts
│ ├── __test__
│ │ ├── FormSchema.spec.ts
│ │ ├── FormScope.spec.ts
│ │ ├── FormField.spec.ts
│ │ ├── lang.spec.ts
│ │ └── api.spec.ts
│ ├── package.json
│ └── index.ts
├── element-plus
│ ├── util.ts
│ ├── fields
│ │ ├── datatable
│ │ │ └── common.ts
│ │ ├── divider
│ │ │ ├── index.ts
│ │ │ ├── divider.vue
│ │ │ └── setting.vue
│ │ ├── text
│ │ │ ├── index.ts
│ │ │ ├── text.vue
│ │ │ └── setting.vue
│ │ ├── textarea
│ │ │ ├── index.ts
│ │ │ ├── textarea.vue
│ │ │ └── setting.vue
│ │ ├── date
│ │ │ ├── index.ts
│ │ │ ├── date.vue
│ │ │ └── setting.vue
│ │ ├── number
│ │ │ ├── index.ts
│ │ │ └── number.vue
│ │ ├── select
│ │ │ ├── index.ts
│ │ │ └── select.vue
│ │ ├── index.ts
│ │ ├── radio
│ │ │ ├── index.ts
│ │ │ └── radio.vue
│ │ ├── checkbox
│ │ │ ├── index.ts
│ │ │ └── checkbox.vue
│ │ ├── group
│ │ │ ├── setting.vue
│ │ │ └── index.scss
│ │ └── tabs
│ │ │ └── index.scss
│ ├── README.md
│ ├── logic
│ │ ├── index.module.scss
│ │ ├── common.tsx
│ │ ├── index.ts
│ │ ├── date.tsx
│ │ └── number.tsx
│ ├── index.scss
│ ├── index.ts
│ ├── package.json
│ └── FormSetting.vue
├── bootstrap
│ ├── fields
│ │ ├── datatable
│ │ │ └── common.ts
│ │ ├── divider
│ │ │ ├── index.ts
│ │ │ └── divider.vue
│ │ ├── date
│ │ │ ├── index.ts
│ │ │ ├── date.vue
│ │ │ └── setting.vue
│ │ ├── text
│ │ │ ├── index.ts
│ │ │ ├── text.vue
│ │ │ └── setting.vue
│ │ ├── textarea
│ │ │ ├── index.ts
│ │ │ ├── textarea.vue
│ │ │ └── setting.vue
│ │ ├── select
│ │ │ ├── index.ts
│ │ │ └── select.vue
│ │ ├── index.ts
│ │ ├── radio
│ │ │ ├── index.ts
│ │ │ └── radio.vue
│ │ ├── checkbox
│ │ │ ├── index.ts
│ │ │ └── checkbox.vue
│ │ ├── group
│ │ │ ├── setting.vue
│ │ │ └── index.scss
│ │ ├── number
│ │ │ ├── setting.vue
│ │ │ └── index.tsx
│ │ └── tabs
│ │ │ └── index.scss
│ ├── README.md
│ ├── logic
│ │ ├── common.tsx
│ │ ├── index.tsx
│ │ ├── date.tsx
│ │ └── number.tsx
│ ├── index.ts
│ ├── package.json
│ ├── index.scss
│ ├── FormSetting.vue
│ ├── util.ts
│ └── FieldLogic.vue
├── common
│ ├── svg
│ │ ├── raw.js
│ │ ├── select.svg
│ │ ├── textarea.svg
│ │ ├── text.svg
│ │ ├── radio.svg
│ │ ├── remove.svg
│ │ ├── info.svg
│ │ ├── pickup.svg
│ │ ├── checkbox.svg
│ │ ├── number.svg
│ │ ├── pick.svg
│ │ ├── group.svg
│ │ ├── divider.svg
│ │ ├── clone.svg
│ │ ├── trash.svg
│ │ ├── datatable.svg
│ │ ├── date.svg
│ │ └── tabs.svg
│ ├── button.ts
│ └── __test__
│ │ └── operator.spec.ts
├── antdv
│ └── index.ts
├── props.d.ts
└── global.d.ts
├── docs
├── img
│ └── ac762befe393daba8318.png
├── 305.6de560aa.js
├── index.html
└── 55.9b82bab6.js
├── document
├── assets
│ ├── image
│ │ └── architecture.png
│ ├── style
│ │ └── document
│ │ │ ├── index.css
│ │ │ ├── _vars.css
│ │ │ ├── article.css
│ │ │ └── highlight.css
│ └── svg
│ │ ├── outbound-dark.svg
│ │ ├── outbound.svg
│ │ ├── github.svg
│ │ └── github-dark.svg
├── views
│ ├── example
│ │ ├── bootstrap
│ │ │ ├── index.ts
│ │ │ └── index.scss
│ │ ├── element-plus
│ │ │ ├── index.ts
│ │ │ └── index.scss
│ │ └── viewer.vue
│ ├── not-found.vue
│ └── document
│ │ ├── index.ts
│ │ └── menus.ts
├── native
│ ├── index.ts
│ ├── codebox
│ │ ├── index.css
│ │ └── index.ts
│ └── is-link.ts
├── app.vue
├── docs
│ ├── concept.md
│ ├── components
│ │ ├── XFormItem.md
│ │ ├── XFormViewer.md
│ │ ├── XFormBuilder.md
│ │ └── XFormDesigner.md
│ ├── introduction.md
│ ├── index.ts
│ └── model.md
├── component
│ ├── index.ts
│ ├── FooterGuide.vue
│ └── Notification
│ │ └── index.scss
├── index.ts
├── index.html
├── router.ts
└── util
│ └── enhance.ts
├── postcss.config.js
├── example
├── index.css
├── bootstrap.html
├── element.html
├── bootstrap.esm.html
└── element.esm.html
├── babel.config.js
├── jest.config.js
├── tsconfig.json
├── api-extractor.json
├── README.md
└── LICENSE
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
--------------------------------------------------------------------------------
/scripts/build.types.js:
--------------------------------------------------------------------------------
1 | require('./utils').buildTypes()
--------------------------------------------------------------------------------
/scripts/clean.js:
--------------------------------------------------------------------------------
1 | require('./utils').cleanAll()
2 |
--------------------------------------------------------------------------------
/scripts/build.document.js:
--------------------------------------------------------------------------------
1 | require('./utils').buildDocument()
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run lint
5 |
--------------------------------------------------------------------------------
/scripts/env/development.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | website: {
3 | base: '/'
4 | }
5 | }
--------------------------------------------------------------------------------
/scripts/release.js:
--------------------------------------------------------------------------------
1 | require('./utils').release().catch(err => {
2 | console.error(err)
3 | })
--------------------------------------------------------------------------------
/scripts/env/production.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | website: {
3 | base: '/xForm/'
4 | }
5 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .vscode
4 | __private__
5 | coverage
6 | types
7 | example/packages
--------------------------------------------------------------------------------
/scripts/build.code.js:
--------------------------------------------------------------------------------
1 | const utils = require('./utils')
2 |
3 | utils.cleanAll()
4 | utils.buildCode()
--------------------------------------------------------------------------------
/packages/core/util/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lang'
2 | export * from './dom'
3 | export * from './component'
4 |
--------------------------------------------------------------------------------
/docs/img/ac762befe393daba8318.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongls/xForm/HEAD/docs/img/ac762befe393daba8318.png
--------------------------------------------------------------------------------
/document/assets/image/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongls/xForm/HEAD/document/assets/image/architecture.png
--------------------------------------------------------------------------------
/packages/core/assets/img/xform-tip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongls/xForm/HEAD/packages/core/assets/img/xform-tip.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-import'),
4 | require('autoprefixer')
5 | ]
6 | }
--------------------------------------------------------------------------------
/packages/core/README.md:
--------------------------------------------------------------------------------
1 | # xForm
2 | `xForm`核心组件库。
3 |
4 | - [文档](https://dongls.github.io/xForm/)
5 |
6 | ## License
7 | [MIT](LICENSE)
--------------------------------------------------------------------------------
/document/views/example/bootstrap/index.ts:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 | import Bootstrap from '@bootstrap/index'
3 |
4 | export default Bootstrap
--------------------------------------------------------------------------------
/packages/core/api/Exports.ts:
--------------------------------------------------------------------------------
1 | import * as api from './index'
2 |
3 | export * from './index'
4 | export function useApi(){
5 | return api
6 | }
--------------------------------------------------------------------------------
/document/views/example/element-plus/index.ts:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 | import ElementPlus from '@element-plus/index'
3 |
4 | export default ElementPlus
--------------------------------------------------------------------------------
/document/native/index.ts:
--------------------------------------------------------------------------------
1 | import CodeBox from './codebox/index'
2 | import IsLink from './is-link'
3 |
4 | [
5 | CodeBox,
6 | IsLink
7 | ].forEach(c => c.install())
--------------------------------------------------------------------------------
/packages/element-plus/util.ts:
--------------------------------------------------------------------------------
1 | export {
2 | useValue,
3 | useDefaultValueApi,
4 | useFieldProp,
5 | useOptions,
6 | useSchemaProp
7 | } from '@common/util'
8 |
--------------------------------------------------------------------------------
/scripts/jest/raw-loader.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | process(src) {
3 | return {
4 | code: 'module.exports = ' + JSON.stringify(src) + ';'
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/document/assets/style/document/index.css:
--------------------------------------------------------------------------------
1 | @import './_vars.css';
2 |
3 | @import './base.css';
4 | @import './highlight.css';
5 | @import './doc.css';
6 | @import './article.css';
--------------------------------------------------------------------------------
/document/views/not-found.vue:
--------------------------------------------------------------------------------
1 |
2 | 😂 在写了!在写了!在写了~~~
3 |
4 |
5 |
--------------------------------------------------------------------------------
/scripts/webpack/loaders/markdown-loader/extend-html.js:
--------------------------------------------------------------------------------
1 | const blocks = require('markdown-it/lib/common/html_blocks');
2 |
3 | [
4 | 'md-meta',
5 | 'code-box'
6 | ].forEach(b => blocks.push(b))
7 |
--------------------------------------------------------------------------------
/docs/305.6de560aa.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkxform=self.webpackChunkxform||[]).push([[305],{8305:(n,e,s)=>{s.r(e),s.d(e,{default:()=>t});const t={name:"antdv",version:"0.8.1",install:function(){}}}}]);
--------------------------------------------------------------------------------
/scripts/jest/setup.js:
--------------------------------------------------------------------------------
1 | window.__VERSION__ = require('../../package.json').version
2 | window.__IS_TEST__ = true
3 | window.__IS_DEV__ = false
4 |
5 | HTMLElement.prototype.scrollIntoView = function(){ /** */}
--------------------------------------------------------------------------------
/document/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/packages/core/component/FormBuilder/component.css:
--------------------------------------------------------------------------------
1 | .xform-builder{
2 | background-color: #fff;
3 |
4 | font-size: var(--xform-font-size);
5 | color: var(--xform-text-color);
6 | padding: 0 10px;
7 | margin: 0 auto;
8 | }
--------------------------------------------------------------------------------
/document/views/document/index.ts:
--------------------------------------------------------------------------------
1 | import '@document/assets/style/document/index.css'
2 | import '@document/native/index'
3 |
4 | import Doc from './doc.vue'
5 | import Page from './page.vue'
6 |
7 | export {
8 | Doc,
9 | Page
10 | }
--------------------------------------------------------------------------------
/packages/bootstrap/fields/datatable/common.ts:
--------------------------------------------------------------------------------
1 | import { FormField } from '@dongls/xform'
2 |
3 | export type Row = {[prop: string]: FormField}
4 |
5 | export const DEF_COLUMN_WIDTH = 150
6 | export const BODY_CLASS = 'xform-bs-datatable-columns'
--------------------------------------------------------------------------------
/packages/element-plus/fields/datatable/common.ts:
--------------------------------------------------------------------------------
1 | import { FormField } from '@dongls/xform'
2 |
3 | export type Row = {[prop: string]: FormField}
4 |
5 | export const DEF_COLUMN_WIDTH = 150
6 | export const BODY_CLASS = 'xform-el-datatable-columns'
--------------------------------------------------------------------------------
/packages/element-plus/README.md:
--------------------------------------------------------------------------------
1 | # ElementPlus For xForm
2 | 基于[Element Plus][element]的[xForm][xForm]字段库。
3 |
4 | ## License
5 | [MIT](LICENSE)
6 |
7 | [element]: https://github.com/element-plus/element-plus
8 | [xForm]: https://github.com/dongls/xForm
--------------------------------------------------------------------------------
/document/views/example/bootstrap/index.scss:
--------------------------------------------------------------------------------
1 | .example-designer-tool-left {
2 | .form-check{
3 | height: 24px;
4 | line-height: 24px;
5 | margin-right: 10px;
6 | }
7 |
8 | .form-check-input{
9 | margin-top: 5px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/scripts/types.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "emitDeclarationOnly": true
6 | },
7 | "include": [
8 | "../packages/**.d.ts",
9 | "../packages/core/index.ts"
10 | ]
11 | }
--------------------------------------------------------------------------------
/document/docs/concept.md:
--------------------------------------------------------------------------------
1 | # 概念
2 |
3 | 通常来说,对于表单设计器,需要提供以下配置:
4 | - **表单设置组件** - 用于设置表单的属性,通过`preset.slots.setting`或者name为`setting`的`slot`提供。
5 | - **字段预览组件** - 用于预览字段,通过以下位置:`FieldConf.preview`,`FieldConf.build`按顺序提供。
6 | - **字段设置组件** - 用于设置字段的属性,通过`FieldConf.setting`提供。
7 |
--------------------------------------------------------------------------------
/packages/common/svg/raw.js:
--------------------------------------------------------------------------------
1 | import IconClone from '@common/svg/clone.svg?raw-loader'
2 | import IconRemove from '@common/svg/remove.svg?raw-loader'
3 | import IconPickUp from '@common/svg/pickup.svg?raw-loader'
4 |
5 | export {
6 | IconClone,
7 | IconRemove,
8 | IconPickUp
9 | }
--------------------------------------------------------------------------------
/packages/antdv/index.ts:
--------------------------------------------------------------------------------
1 | import { FormPreset } from '@dongls/xform'
2 |
3 | const antdv: FormPreset = {
4 | name: 'antdv',
5 | version: __VERSION__,
6 | install(){
7 | // TODO: antdv
8 | // https://github.com/vueComponent/ant-design-vue
9 | }
10 | }
11 |
12 | export default antdv
--------------------------------------------------------------------------------
/packages/core/index.css:
--------------------------------------------------------------------------------
1 | @import './assets/css/variables.css';
2 | @import './assets/css/common.css';
3 |
4 | @import './component/FormBuilder/component.css';
5 | @import './component/FormDesigner/component.css';
6 | @import './component/FormItem/component.css';
7 | @import './component/FormViewer/component.css';
--------------------------------------------------------------------------------
/packages/core/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './action'
2 | export * from './common'
3 | export * from './constant'
4 | export * from './drag'
5 | export * from './Field'
6 | export * from './FormField'
7 | export * from './FormSchema'
8 | export * from './Serializable'
9 | export * from './FormScope'
10 | export * from './Button'
11 |
--------------------------------------------------------------------------------
/document/assets/style/document/_vars.css:
--------------------------------------------------------------------------------
1 | :root{
2 | --doc-color-primary: rgb(0,123,255);
3 |
4 | --doc-text-color-primary: rgb(36, 41, 46);
5 | --doc-head-text-color: rgb(33, 36, 51);
6 |
7 | --doc-link-color: rgb(0,123,255);
8 | --doc-link-hover-bg-color: rgba(0, 76, 252, .1);
9 |
10 | --doc-main-width: 1180px;
11 | }
12 |
--------------------------------------------------------------------------------
/scripts/dev.js:
--------------------------------------------------------------------------------
1 | const execa = require('execa')
2 | const utils = require('./utils')
3 |
4 | utils.cleanAll()
5 |
6 | execa.sync(
7 | 'node_modules/.bin/webpack',
8 | [
9 | 'serve',
10 | '--config',
11 | 'scripts/webpack/webpack.document.config.js',
12 | ],
13 | {
14 | stdio: 'inherit',
15 | env: { 'NODE_ENV': 'development' }
16 | }
17 | )
18 |
--------------------------------------------------------------------------------
/packages/props.d.ts:
--------------------------------------------------------------------------------
1 | import { FormSchema, FormField } from './core'
2 | import { FormScope } from './core/model'
3 |
4 | declare global {
5 | interface Element{
6 | __PROP_XFORM_DRAGE_MODE__: string
7 | __PROP_XFORM_FIELD__: FormField
8 | __PROP_XFORM_FIELD_TYPE__: string
9 | __PROP_XFORM_SCHEMA__: FormSchema
10 | __PROP_XFORM_SCOPE__: FormScope
11 | }
12 | }
--------------------------------------------------------------------------------
/example/index.css:
--------------------------------------------------------------------------------
1 | body{
2 | margin: 0;
3 | display: flex;
4 | }
5 |
6 | aside{
7 | width: 200px;
8 | border-right: 1px solid #aaa;
9 | padding: 5px;
10 | overflow: auto;
11 | }
12 |
13 | aside a{
14 | display: block;
15 | color: rgb(0, 0, 238);
16 | }
17 |
18 | aside a + a{
19 | margin-top: 4px;
20 | }
21 |
22 | iframe{
23 | display: block;
24 | border: none;
25 | flex: 1;
26 | }
--------------------------------------------------------------------------------
/packages/core/model/Exports.ts:
--------------------------------------------------------------------------------
1 | export {
2 | FormBuilderApi,
3 | FormBuilderContext,
4 | FormDesignerApi,
5 | FormDesignerContext,
6 | FormPreset,
7 | FormRenderContext,
8 | FormViewerContext,
9 | RawProps,
10 | Button,
11 | Icon
12 | } from './common'
13 |
14 | export { Field, FieldLogic } from './Field'
15 | export { FormField, FormFieldLogic } from './FormField'
16 | export { FormSchema } from './FormSchema'
17 |
--------------------------------------------------------------------------------
/document/component/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from 'vue'
2 |
3 | import Modal from './Modal.vue'
4 | import FooterGuide from './FooterGuide.vue'
5 | import PresetPicker from './PresetPicker.vue'
6 |
7 | export { useNotification } from './Notification'
8 |
9 | export default function(app: App){
10 | app.component(Modal.name, Modal)
11 | app.component(FooterGuide.name, FooterGuide)
12 | app.component(PresetPicker.name, PresetPicker)
13 | }
--------------------------------------------------------------------------------
/document/assets/svg/outbound-dark.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/document/assets/svg/outbound.svg:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/packages/common/svg/select.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/core/component/FormViewer/component.css:
--------------------------------------------------------------------------------
1 | .xform-viewer{
2 | font-size: var(--xform-font-size);
3 | color: var(--xform-text-color);
4 | background-color: #fff;
5 | margin: 0 auto;
6 | padding: 0 10px;
7 | }
8 |
9 | .xform-viewer .xform-item-label > span:before{
10 | content: none !important;
11 | }
12 |
13 | .xform-viewer-value{
14 | line-height: 21px;
15 | display: inline-block;
16 | padding-top: 5px;
17 | padding-bottom: 5px;
18 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const RELEASE_TARGET = process.env.RELEASE_TARGET
2 | const FOR_BUNDLER = RELEASE_TARGET === 'bundler'
3 |
4 | const presets = (
5 | FOR_BUNDLER
6 | ? []
7 | : [
8 | ['@babel/preset-env',{
9 | browserslistEnv: RELEASE_TARGET,
10 | useBuiltIns: 'usage',
11 | corejs: 3,
12 | }]
13 | ]
14 | )
15 |
16 | const plugins = ['@vue/babel-plugin-jsx']
17 |
18 | module.exports = { presets, plugins }
--------------------------------------------------------------------------------
/packages/common/svg/textarea.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/bootstrap/README.md:
--------------------------------------------------------------------------------
1 | # Bootstrap For xForm
2 | 基于[Bootstrap@5.x][bootstrap]的[xForm][xForm]字段库。
3 |
4 | ## 已支持字段
5 | - checkbox - 多选
6 | - datatable - 数据表格
7 | - date - 日期
8 | - divider - 分割线
9 | - group - 分组
10 | - number - 数字
11 | - radio - 单选
12 | - select - 下拉选择
13 | - tabs - 标签页
14 | - text - 单行文本
15 | - textarea - 多行文本
16 |
17 | ## License
18 | [MIT](LICENSE)
19 |
20 |
21 | [bootstrap]: https://github.com/twbs/bootstrap
22 | [xForm]: https://github.com/dongls/xForm
23 |
--------------------------------------------------------------------------------
/packages/core/assets/css/variables.css:
--------------------------------------------------------------------------------
1 | :root{
2 | --xform-color-primary: #409eff;
3 | --xform-color-primary-lighten: #e8f3ff;
4 |
5 | --xform-color-danger: #f56c6c;
6 | --xform-color-warning: #e6a23c;
7 |
8 | --xform-border-color: #eee;
9 |
10 | --xform-text-color: #343a40;
11 | --xform-text-color-secondary: #9a9a9a;
12 |
13 | --xform-font-size: 14px;
14 |
15 | --xform-label-width: 120px;
16 |
17 | --xform-designer-mark-color: red;
18 | --xform-designer-responsive-width: 640px;
19 | }
--------------------------------------------------------------------------------
/document/index.ts:
--------------------------------------------------------------------------------
1 | import '../packages/core/index.css'
2 |
3 | import * as Vue from 'vue'
4 |
5 | import xForm from '@dongls/xform'
6 | import App from './app.vue'
7 | import router from './router'
8 | import component from './component/index'
9 |
10 | const app = Vue.createApp(App)
11 |
12 | app.use(router)
13 | app.use(xForm)
14 | app.use(component)
15 | app.mount('#app')
16 |
17 | app.config.performance = __IS_DEV__
18 | app.config.globalProperties.IS_DEV = __IS_DEV__
19 |
20 | // 暴露Vue对象给外部引入的库使用
21 | ;(window as any).Vue = Vue
--------------------------------------------------------------------------------
/document/native/codebox/index.css:
--------------------------------------------------------------------------------
1 | @import '../../assets/style/document/highlight.css';
2 |
3 | .code-box-root pre{
4 | margin: 0;
5 | }
6 |
7 | .code-box-root:hover .toolbox > button{
8 | visibility: visible;
9 | }
10 |
11 | .toolbox{
12 | position: absolute;
13 | bottom: 5px;
14 | right: 5px;
15 | text-align: right;
16 | }
17 |
18 | .toolbox button{
19 | visibility: hidden;
20 | background-color: transparent;
21 | border: none;
22 | cursor: pointer;
23 | outline: none;
24 | color: rgb(0,123,255);
25 | }
26 |
--------------------------------------------------------------------------------
/document/native/is-link.ts:
--------------------------------------------------------------------------------
1 | import icon from '!!raw-loader!../assets/svg/outbound.svg'
2 |
3 | class IconOutbound extends HTMLElement {
4 | constructor(){
5 | super()
6 |
7 | this.innerHTML = icon
8 | this.title = '查看详情'
9 |
10 | this.addEventListener('click', function(){
11 | const path = this.getAttribute('path')
12 | window.open(window.location.origin + path)
13 | })
14 | }
15 | }
16 |
17 |
18 | export default {
19 | install(){
20 | customElements.define('is-link', IconOutbound)
21 | }
22 | }
--------------------------------------------------------------------------------
/document/views/example/element-plus/index.scss:
--------------------------------------------------------------------------------
1 | .example-designer-tool{
2 | .el-button--small{
3 | font-size: 14px;
4 | }
5 |
6 | .el-button + .el-button{
7 | margin-left: 15px;
8 | }
9 | }
10 |
11 | .example-builder-footer{
12 | .el-button--small{
13 | font-size: 14px;
14 | }
15 | }
16 |
17 | .example-el-confirm strong{
18 | margin: 0 2px;
19 |
20 | &[danger]{
21 | color: #F56C6C;
22 | }
23 |
24 | &[info]{
25 | color: #409EFF;
26 | }
27 | }
28 |
29 | .example-el-notify{
30 | .example-schema-error{
31 | padding-left: 0;
32 | }
33 | }
--------------------------------------------------------------------------------
/packages/element-plus/logic/index.module.scss:
--------------------------------------------------------------------------------
1 | .logicForm{
2 | display: flex;
3 | flex-flow: row nowrap;
4 | align-items: center;
5 | height: 30px;
6 | line-height: 30px;
7 |
8 | strong{
9 | margin: 0 4px;
10 | }
11 |
12 | & > :global(.el-select){
13 | width: 100px;
14 | margin-left: 10px;
15 | }
16 |
17 | & > :global(.el-input){
18 | width: 150px;
19 | margin-left: 10px;
20 | }
21 |
22 | & > :global(.el-input-number){
23 | width: 150px;
24 | margin-left: 10px;
25 | }
26 |
27 | & > .valueSelect{
28 | width: 150px;
29 | }
30 | }
--------------------------------------------------------------------------------
/packages/bootstrap/logic/common.tsx:
--------------------------------------------------------------------------------
1 | import classes from './index.module.scss'
2 | import { FormField, FormFieldLogic, useConstant } from '@dongls/xform'
3 |
4 | const { CLASS } = useConstant()
5 |
6 | export function createWarnTip(logic: FormFieldLogic, field: FormField){
7 | const targetField = field.parent.find(logic.field)
8 | if(targetField == null) return
当前逻辑失效,目标字段被删除或位置发生变化
9 |
10 | const title = {targetField.title}
11 | return 当前逻辑失效,目标字段{title}的位置发生变化
12 | }
--------------------------------------------------------------------------------
/packages/bootstrap/fields/divider/index.ts:
--------------------------------------------------------------------------------
1 | import { Field } from '@dongls/xform'
2 | import icon from '@common/svg/divider.svg'
3 |
4 | import divider from './divider.vue'
5 | import setting from './setting.vue'
6 |
7 | export default Field.create({
8 | icon: icon,
9 | type: 'divider',
10 | title: '分割线',
11 | custom: true,
12 | setting: setting,
13 | build: divider,
14 | view: divider,
15 | onCreate(field, params, init){
16 | if(init){
17 | field.attributes.type = 'solid'
18 | field.attributes.top = 0
19 | field.attributes.bottom = 0
20 | }
21 | },
22 | })
--------------------------------------------------------------------------------
/packages/common/svg/text.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/element-plus/logic/common.tsx:
--------------------------------------------------------------------------------
1 | import classes from './index.module.scss'
2 | import { FormField, FormFieldLogic, useConstant } from '@dongls/xform'
3 |
4 | const { CLASS } = useConstant()
5 |
6 | export function createWarnTip(logic: FormFieldLogic, field: FormField){
7 | const targetField = field.parent.find(logic.field)
8 | if(targetField == null) return 当前逻辑失效,目标字段被删除或位置发生变化
9 |
10 | const title = {targetField.title}
11 | return 当前逻辑失效,目标字段{title}的位置发生变化
12 | }
--------------------------------------------------------------------------------
/packages/element-plus/fields/divider/index.ts:
--------------------------------------------------------------------------------
1 | import { Field } from '@dongls/xform'
2 | import icon from '@common/svg/divider.svg'
3 |
4 | import divider from './divider.vue'
5 | import setting from './setting.vue'
6 |
7 | export default Field.create({
8 | icon: icon,
9 | type: 'divider',
10 | title: '分割线',
11 | custom: true,
12 | setting: setting,
13 | build: divider,
14 | view: divider,
15 | onCreate(field, params, init){
16 | if(init){
17 | field.attributes.type = 'solid'
18 | field.attributes.top = 0
19 | field.attributes.bottom = 0
20 | }
21 | },
22 | })
--------------------------------------------------------------------------------
/packages/bootstrap/fields/date/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { DATE_VALUE_COMPARE } from '@bootstrap/logic'
3 |
4 | import icon from '@common/svg/date.svg'
5 | import date from './date.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'date',
11 | title: '日期',
12 | setting: setting,
13 | build: date,
14 | validator(field, value: string){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | return Promise.resolve()
17 | },
18 | logic: [
19 | DATE_VALUE_COMPARE
20 | ]
21 | })
--------------------------------------------------------------------------------
/packages/element-plus/index.scss:
--------------------------------------------------------------------------------
1 | .xform-setting + .xform-setting{
2 | margin-top: 10px;
3 | }
4 |
5 | .xform-setting header{
6 | font-weight: 700;
7 | }
8 |
9 | .xform-el-setting-option{
10 | display: flex;
11 | flex-flow: row nowrap;
12 | align-items: center;
13 | margin-bottom: 5px;
14 |
15 | .el-button{
16 | margin-left: 5px;
17 | }
18 | }
19 |
20 | .xform-el-empty-tip{
21 | position: absolute;
22 | top: 45%;
23 | left: 50%;
24 | transform: translateX(-50%);
25 | margin: 0;
26 | text-align: center;
27 | color: #9a9a9a;
28 | font-size: 14px;
29 | font-weight: 600;
30 | }
31 |
--------------------------------------------------------------------------------
/packages/core/api/test/Preset.spec.ts:
--------------------------------------------------------------------------------
1 | import { reset, isImmediateValidate } from '..'
2 | import { FormPreset } from '../../model'
3 | import { useApi, usePreset } from '../Exports'
4 |
5 | describe('usePreset', () => {
6 | test('with config', () => {
7 | const preset: FormPreset = {
8 | name: 'test',
9 | install(){
10 | const api = useApi()
11 | api.useConfig({
12 | validation: {
13 | immediate: false
14 | }
15 | })
16 | }
17 | }
18 |
19 | reset()
20 | usePreset(preset)
21 |
22 | expect(isImmediateValidate()).toBe(false)
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/packages/bootstrap/index.ts:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import { FormPreset, registerSlot, removeSlot } from '@dongls/xform'
4 |
5 | import logic from './logic'
6 | import fields from './fields'
7 |
8 | import FormSetting from './FormSetting.vue'
9 |
10 | const bootstrap: FormPreset = {
11 | name: 'bootstrap',
12 | version: __VERSION__,
13 | install(){
14 | registerSlot('setting_form', FormSetting)
15 |
16 | fields.use()
17 | logic.use()
18 |
19 | return function(){
20 | removeSlot('setting_form')
21 |
22 | fields.remove()
23 | logic.remove()
24 | }
25 | }
26 | }
27 |
28 | export default bootstrap
--------------------------------------------------------------------------------
/packages/core/api/Slots.ts:
--------------------------------------------------------------------------------
1 | import { ComponentOptions } from 'vue'
2 | import { isObject } from '../util'
3 | import { store } from './Store'
4 |
5 | export function registerSlots(o: { [prop: string]: ComponentOptions }){
6 | if(!isObject(o)) return
7 |
8 | for(const key in o){
9 | const slot = o[key]
10 | registerSlot(key, slot)
11 | }
12 | }
13 |
14 | export function registerSlot(key: string, slot: ComponentOptions){
15 | store.slots.set(key, slot)
16 | }
17 |
18 | export function getSlot(key: string){
19 | return store.slots.get(key)
20 | }
21 |
22 | export function removeSlot(key: string){
23 | store.slots.delete(key)
24 | }
--------------------------------------------------------------------------------
/packages/element-plus/index.ts:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import { FormPreset, registerSlot, removeSlot } from '@dongls/xform'
4 |
5 | import logic from './logic'
6 | import fields from './fields'
7 |
8 | import FormSetting from './FormSetting.vue'
9 |
10 | const elementPlus: FormPreset = {
11 | name: 'element-plus',
12 | version: __VERSION__,
13 | install(){
14 | registerSlot('setting_form', FormSetting)
15 |
16 | fields.use()
17 | logic.use()
18 |
19 | return function(){
20 | removeSlot('setting_form')
21 |
22 | fields.remove()
23 | logic.remove()
24 | }
25 | }
26 | }
27 |
28 | export default elementPlus
--------------------------------------------------------------------------------
/packages/core/api/Preset.ts:
--------------------------------------------------------------------------------
1 | import { FormPreset } from '../model'
2 | import { store } from './Store'
3 | import { isFunction } from '../util'
4 |
5 | export function usePreset(preset: FormPreset, options?: any){
6 | if(null == preset || !isFunction(preset.install)) return
7 |
8 | store.preset = {
9 | name: preset.name,
10 | version: preset.version,
11 | cleanup: preset.install(options)
12 | }
13 | }
14 |
15 | export function resetPreset(){
16 | if(store.preset == null) return
17 | if(isFunction(store.preset.cleanup)) store.preset.cleanup()
18 |
19 | store.preset = null
20 | }
21 |
22 | export function getPreset(){
23 | return store.preset
24 | }
25 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/text/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { STRING_LENGTH_COMPARE, STRING_VALUE_COMPARE } from '@element-plus/logic/string'
3 |
4 | import icon from '@common/svg/text.svg'
5 | import text from './text.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'text',
11 | title: '单行文本',
12 | setting: setting,
13 | build: text,
14 | validator(field, value){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | return Promise.resolve()
17 | },
18 | logic: [
19 | STRING_VALUE_COMPARE,
20 | STRING_LENGTH_COMPARE
21 | ]
22 | })
--------------------------------------------------------------------------------
/packages/bootstrap/fields/text/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import {
3 | STRING_VALUE_COMPARE,
4 | STRING_LENGTH_COMPARE
5 | } from '@bootstrap/logic'
6 |
7 | import icon from '@common/svg/text.svg'
8 | import text from './text.vue'
9 | import setting from './setting.vue'
10 |
11 | export default Field.create({
12 | icon: icon,
13 | type: 'text',
14 | title: '单行文本',
15 | setting: setting,
16 | build: text,
17 | validator(field, value){
18 | if(field.required && isEmpty(value)) return Promise.reject('必填')
19 | return Promise.resolve()
20 | },
21 | logic: [
22 | STRING_VALUE_COMPARE,
23 | STRING_LENGTH_COMPARE
24 | ]
25 | })
--------------------------------------------------------------------------------
/packages/element-plus/fields/textarea/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { STRING_LENGTH_COMPARE, STRING_VALUE_COMPARE } from '@element-plus/logic/string'
3 |
4 | import icon from '@common/svg/textarea.svg'
5 | import textarea from './textarea.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'textarea',
11 | title: '多行文本',
12 | setting: setting,
13 | build: textarea,
14 | validator(field, value){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | return Promise.resolve()
17 | },
18 | logic: [
19 | STRING_VALUE_COMPARE,
20 | STRING_LENGTH_COMPARE
21 | ]
22 | })
--------------------------------------------------------------------------------
/packages/bootstrap/fields/textarea/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import {
3 | STRING_VALUE_COMPARE,
4 | STRING_LENGTH_COMPARE
5 | } from '@bootstrap/logic'
6 |
7 | import icon from '@common/svg/textarea.svg'
8 | import textarea from './textarea.vue'
9 | import setting from './setting.vue'
10 |
11 | export default Field.create({
12 | icon: icon,
13 | type: 'textarea',
14 | title: '多行文本',
15 | setting: setting,
16 | build: textarea,
17 | validator(field, value){
18 | if(field.required && isEmpty(value)) return Promise.reject('必填')
19 | return Promise.resolve()
20 | },
21 | logic: [
22 | STRING_VALUE_COMPARE,
23 | STRING_LENGTH_COMPARE
24 | ]
25 | })
--------------------------------------------------------------------------------
/document/assets/svg/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/document/docs/components/XFormItem.md:
--------------------------------------------------------------------------------
1 | # XFormItem
2 | 通常情况下,`XFormDesigner`、`XFormBuilder`和`XFormViewer`会默认使用该组件包裹每一个字段所对应的表单组件,**用于提供统一的布局和表单验证**。当你需要提供某些特殊的表单字段时,你需要使用该组件将字段包装,以便提供统一的验证。例如,需要在`XFormBuilder`中插入一些固定的表单字段:
3 | ```html
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ```
15 |
16 | ## Props
17 | ### field
18 | - **类型**:`XField`
19 | - **说明**:字段配置。
20 | ### validation
21 | - **类型**:`boolean | () => Promise`
22 | - **说明**:字段配置。
--------------------------------------------------------------------------------
/document/assets/svg/github-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/core/__test__/FormSchema.spec.ts:
--------------------------------------------------------------------------------
1 | import { createSchema } from '../api'
2 | import { getPrivateProps } from '../util/lang'
3 |
4 | test('validate', async () => {
5 | const schema = createSchema({
6 | fields: [
7 | {},
8 | { title: '字段1' },
9 | { title: '字段2', fields: [{}] },
10 | { title: '字段3', fields: [{ title: '字段3-1', fields: [{}] }] }
11 | ]
12 | })
13 |
14 | const r = await schema.validate()
15 | expect(r.valid).toBe(false)
16 | expect(r.result.length).toBe(3)
17 | const result = r.result
18 | expect(result[2].fields[0].fields[0].valid).toBe(false)
19 | })
20 |
21 | test('FormSchema: private props', () => {
22 | const schema = createSchema()
23 | expect(() => getPrivateProps(schema, Symbol())).toThrow()
24 | })
--------------------------------------------------------------------------------
/packages/core/api/Config.ts:
--------------------------------------------------------------------------------
1 | import { FormConfig } from '../model'
2 | import { clonePlainObject, mergePlainObject } from '../util'
3 | import { store, createConfig } from './Store'
4 |
5 | // TODO: 使用Proxy重构
6 | export function useConfig(config: FormConfig){
7 | if(config == null) return
8 |
9 | const clone = clonePlainObject(config)
10 | mergePlainObject(store.config, clone)
11 |
12 | if(store.config.logic === true){
13 | console.warn('[xForm]: 字段逻辑目前为实验性功能,未来可能会发生变更,请谨慎使用!')
14 | }
15 | }
16 |
17 | export function resetConfig(){
18 | store.config = createConfig()
19 | }
20 |
21 | export function getConfig(){
22 | return store.config
23 | }
24 |
25 | export function isImmediateValidate(){
26 | return store.config.validation.immediate !== false
27 | }
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "ts-jest",
3 | transform: {
4 | "^.+\\.vue$": "@vue/vue3-jest",
5 | "^.+\\.js$": "babel-jest",
6 | "^.+\\.(svg|png)$": "/scripts/jest/raw-loader.js",
7 | '^.+\\.tsx?$': ['ts-jest', { babelConfig: true }]
8 | },
9 | moduleFileExtensions: ["vue", "js", "jsx", "ts", "tsx"],
10 | moduleNameMapper: {
11 | "@common/(.*)": "/packages/common/$1",
12 | "@dongls/xform": "/packages/core/index.ts",
13 | "\\.module.css$": "identity-obj-proxy"
14 | },
15 | setupFiles: [
16 | '/scripts/jest/setup.js'
17 | ],
18 | testEnvironment: "jsdom",
19 | testEnvironmentOptions: {
20 | customExportConditions: [
21 | 'node',
22 | 'node-addons'
23 | ]
24 | },
25 | }
--------------------------------------------------------------------------------
/packages/bootstrap/fields/divider/divider.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
30 |
--------------------------------------------------------------------------------
/packages/bootstrap/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@dongls/xform.bootstrap",
3 | "version": "0.8.1",
4 | "description": "基于Bootstrap的xForm字段库。",
5 | "main": "dist/index.js",
6 | "style": "dist/index.css",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/dongls/xForm.git"
10 | },
11 | "homepage": "https://github.com/dongls/xForm",
12 | "keywords": [
13 | "vue",
14 | "form",
15 | "xform"
16 | ],
17 | "author": {
18 | "name": "dongls",
19 | "email": "173110115@qq.com"
20 | },
21 | "license": "MIT",
22 | "files": [
23 | "dist"
24 | ],
25 | "peerDependencies": {
26 | "@dongls/xform": "0.8.1"
27 | },
28 | "publishConfig": {
29 | "registry": "https://registry.npmjs.org",
30 | "access": "public"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@dongls/xform",
3 | "version": "0.8.1",
4 | "description": "Vue驱动的自定义表单套件。",
5 | "main": "dist/index.js",
6 | "style": "dist/index.css",
7 | "types": "dist/index.d.ts",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/dongls/xForm.git"
11 | },
12 | "homepage": "https://github.com/dongls/xForm",
13 | "keywords": [
14 | "vue",
15 | "form",
16 | "xform"
17 | ],
18 | "author": {
19 | "name": "dongls",
20 | "email": "173110115@qq.com"
21 | },
22 | "license": "MIT",
23 | "files": [
24 | "dist"
25 | ],
26 | "peerDependencies": {
27 | "vue": "^3.2.21"
28 | },
29 | "publishConfig": {
30 | "registry": "https://registry.npmjs.org",
31 | "access": "public"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/common/svg/radio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/divider/divider.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
30 |
--------------------------------------------------------------------------------
/packages/element-plus/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@dongls/xform.element-plus",
3 | "version": "0.8.1",
4 | "description": "基于Element Plus的xForm字段库。",
5 | "main": "dist/index.js",
6 | "style": "dist/index.css",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/dongls/xForm.git"
10 | },
11 | "homepage": "https://github.com/dongls/xForm",
12 | "keywords": [
13 | "vue",
14 | "form",
15 | "xform"
16 | ],
17 | "author": {
18 | "name": "dongls",
19 | "email": "173110115@qq.com"
20 | },
21 | "license": "MIT",
22 | "files": [
23 | "dist"
24 | ],
25 | "peerDependencies": {
26 | "@dongls/xform": "0.8.1"
27 | },
28 | "publishConfig": {
29 | "registry": "https://registry.npmjs.org",
30 | "access": "public"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/core/api/test/Config.spec.ts:
--------------------------------------------------------------------------------
1 | import { reset } from '..'
2 | import {
3 | resetConfig,
4 | useConfig,
5 | getConfig,
6 | isImmediateValidate,
7 | } from '../Config'
8 |
9 | describe('useConfig', () => {
10 | test('default config', () => {
11 | reset()
12 | const config = getConfig()
13 |
14 | expect(config).not.toBeNull()
15 | expect(config.modes).toBeNull()
16 | expect(config.validation.immediate).toBe(true)
17 | expect(config.genName).toBeInstanceOf(Function)
18 | })
19 |
20 | test('validator.immediate', () => {
21 | resetConfig()
22 | useConfig({ validation: { immediate: false } })
23 |
24 | const config = getConfig()
25 | expect(config.validation.immediate).toBe(false)
26 | expect(isImmediateValidate()).toBe(false)
27 | })
28 | })
29 |
30 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/date/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { DATE_VALUE_COMPARE } from '@element-plus/logic'
3 |
4 | import icon from '@common/svg/date.svg'
5 | import date from './date.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'date',
11 | title: '日期',
12 | setting: setting,
13 | build: date,
14 | onCreate(field){
15 | field.attributes.format = field.attributes.format ?? 'YYYY-MM-DD'
16 | field.attributes.valueFormat = field.attributes.valueFormat ?? 'YYYY-MM-DD'
17 | },
18 | validator(field, value: string){
19 | if(field.required && isEmpty(value)) return Promise.reject('必填')
20 | return Promise.resolve()
21 | },
22 | logic: [
23 | DATE_VALUE_COMPARE
24 | ]
25 | })
--------------------------------------------------------------------------------
/packages/element-plus/fields/text/text.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
34 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/number/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { NUMBER_VALUE_COMPARE } from '@element-plus/logic'
3 |
4 | import icon from '@common/svg/number.svg'
5 | import setting from './setting.vue'
6 | import number from './number.vue'
7 |
8 | export default Field.create({
9 | icon,
10 | type: 'number',
11 | title: '数字',
12 | setting,
13 | build: number,
14 | validator(field, value: number | string){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | if(field.attributes.integer && !/^[-+]?[1-9]?\d+$/.test(value + '')) return Promise.reject('请输入整数')
17 |
18 | return Promise.resolve()
19 | },
20 | onValueInit(field, value){
21 | return parseFloat(value)
22 | },
23 | logic: [
24 | NUMBER_VALUE_COMPARE
25 | ]
26 | })
--------------------------------------------------------------------------------
/packages/bootstrap/fields/date/date.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
35 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/text/text.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
35 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/select/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { OPTION_VALUE_COMPARE } from '@bootstrap/logic'
3 |
4 | import icon from '@common/svg/select.svg'
5 | import select from './select.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'select',
11 | title: '下拉选择',
12 | setting: setting,
13 | build: select,
14 | validator(field, value){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | return Promise.resolve()
17 | },
18 | onCreate(field, params, init){
19 | const options = Array.isArray(params.options) ? params.options : []
20 | if(init) options.push({ value: '选项1' })
21 |
22 | field.options = options
23 | },
24 | logic: [
25 | OPTION_VALUE_COMPARE
26 | ]
27 | })
--------------------------------------------------------------------------------
/packages/element-plus/fields/select/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { OPTION_VALUE_COMPARE } from '@element-plus/logic'
3 |
4 | import icon from '@common/svg/select.svg'
5 | import select from './select.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'select',
11 | title: '下拉选择',
12 | setting: setting,
13 | build: select,
14 | validator(field, value){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | return Promise.resolve()
17 | },
18 | onCreate(field, params, init){
19 | const options = Array.isArray(params.options) ? params.options : []
20 | if(init) options.push({ value: '选项1' })
21 |
22 | field.options = options
23 | },
24 | logic: [
25 | OPTION_VALUE_COMPARE
26 | ]
27 | })
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "baseUrl": ".",
5 | "esModuleInterop": true,
6 | "jsx": "preserve",
7 | "module": "ESNext",
8 | "moduleResolution": "node",
9 | "noImplicitAny": true,
10 | "noImplicitThis": true,
11 | "outDir": "./dist/",
12 | "paths": {
13 | "@common/*": ["packages/common/*"],
14 | "@document/*": ["document/*"],
15 | "@bootstrap/*": ["packages/bootstrap/*"],
16 | "@element-plus/*": ["packages/element-plus/*"],
17 | "@dongls/xform": ["packages/core/index"]
18 | },
19 | "target": "ESNext"
20 | },
21 | "include": [
22 | "packages/**/*.ts",
23 | "packages/**/*.tsx",
24 | "packages/**.d.ts",
25 | "packages/**/*.vue",
26 | "document/**/*.ts",
27 | "document/**/*.tsx",
28 | "document/**/*.vue"
29 | ]
30 | }
--------------------------------------------------------------------------------
/packages/bootstrap/fields/textarea/textarea.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
37 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/index.ts:
--------------------------------------------------------------------------------
1 | import { registerField, removeField } from '@dongls/xform'
2 |
3 | import Checkbox from './checkbox'
4 | import Datatable from './datatable'
5 | import Date from './date'
6 | import Divider from './divider'
7 | import Group from './group'
8 | import Number from './number'
9 | import Radio from './radio'
10 | import Select from './select'
11 | import Tabs from './tabs'
12 | import Text from './text'
13 | import Textarea from './textarea'
14 |
15 | const fields = [
16 | Checkbox,
17 | Datatable,
18 | Date,
19 | Divider,
20 | Group,
21 | Number,
22 | Radio,
23 | Select,
24 | Tabs,
25 | Text,
26 | Textarea,
27 | ]
28 |
29 | export function use(){
30 | registerField(fields)
31 | }
32 |
33 | export function remove(){
34 | fields.forEach(removeField)
35 | }
36 |
37 | export default {
38 | use,
39 | remove
40 | }
--------------------------------------------------------------------------------
/packages/bootstrap/fields/index.ts:
--------------------------------------------------------------------------------
1 | import { registerField, removeField } from '@dongls/xform'
2 |
3 | import Text from './text'
4 | import Textarea from './textarea'
5 | import Number from './number'
6 | import Select from './select'
7 | import Radio from './radio'
8 | import Checkbox from './checkbox'
9 | import Date from './date'
10 | import Divider from './divider'
11 | import Group from './group'
12 | import Tabs from './tabs'
13 | import Datatable from './datatable'
14 |
15 | export const fields = [
16 | Checkbox,
17 | Datatable,
18 | Date,
19 | Divider,
20 | Group,
21 | Number,
22 | Radio,
23 | Select,
24 | Tabs,
25 | Text,
26 | Textarea,
27 | ]
28 |
29 | export function use(){
30 | registerField(fields)
31 | }
32 |
33 | export function remove(){
34 | fields.forEach(removeField)
35 | }
36 |
37 | export default {
38 | use,
39 | remove
40 | }
--------------------------------------------------------------------------------
/document/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | xForm在线示例
9 |
10 |
11 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/textarea/textarea.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
38 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/radio/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { OPTION_VALUE_COMPARE } from '@bootstrap/logic'
3 |
4 | import icon from '@common/svg/radio.svg'
5 | import radio from './radio.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'radio',
11 | title: '单选',
12 | setting: setting,
13 | build: radio,
14 | validator(field, value){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | return Promise.resolve()
17 | },
18 | onCreate(field, params, init){
19 | const options = Array.isArray(params.options) ? params.options : []
20 | if(init) {
21 | options.push({ value: '选项1' })
22 | field.attributes.layout = 'inline'
23 | }
24 |
25 | field.options = options
26 | },
27 | logic: [
28 | OPTION_VALUE_COMPARE
29 | ]
30 | })
--------------------------------------------------------------------------------
/packages/element-plus/fields/radio/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { OPTION_VALUE_COMPARE } from '@element-plus/logic'
3 |
4 | import icon from '@common/svg/radio.svg'
5 | import radio from './radio.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'radio',
11 | title: '单选',
12 | setting: setting,
13 | build: radio,
14 | validator(field, value){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | return Promise.resolve()
17 | },
18 | onCreate(field, params, init){
19 | const options = Array.isArray(params.options) ? params.options : []
20 | if(init) {
21 | options.push({ value: '选项1' })
22 | field.attributes.layout = 'inline'
23 | }
24 |
25 | field.options = options
26 | },
27 | logic: [
28 | OPTION_VALUE_COMPARE
29 | ]
30 | })
--------------------------------------------------------------------------------
/packages/common/svg/remove.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/core/assets/css/common.css:
--------------------------------------------------------------------------------
1 | .xform-is-scroll{
2 | overflow: auto;
3 | scrollbar-color: rgba(200, 200, 200, 0.5) #f5f5f5;
4 | scrollbar-width: thin;
5 | }
6 |
7 | .xform-is-scroll::-webkit-scrollbar {
8 | width: 8px;
9 | height: 8px;
10 | }
11 |
12 | .xform-is-scroll::-webkit-scrollbar-track {
13 | background-color: #f5f5f5;
14 | }
15 |
16 | .xform-is-scroll::-webkit-scrollbar-thumb {
17 | background-color: rgba(200, 200, 200, 0.5);
18 | }
19 |
20 | .xform-is-scroll::-webkit-scrollbar-thumb:hover {
21 | background-color: rgb(200, 200, 200)
22 | }
23 |
24 | .xform-is-hidden{
25 | display: none !important;
26 | }
27 |
28 | .xform-is-unknown{
29 | padding: 5px 0;
30 | line-height: 20px;
31 | margin: 0;
32 | color: var(--xform-text-color-secondary);
33 | }
34 |
35 | .xform-is-warning{
36 | padding: 5px 0;
37 | line-height: 20px;
38 | margin: 0;
39 | color: var(--xform-color-warning);
40 | }
--------------------------------------------------------------------------------
/scripts/webpack/loaders/markdown-loader/highlight.js:
--------------------------------------------------------------------------------
1 | const { escapeHtml } = require('markdown-it/lib/common/utils')
2 | const hljs = require('highlight.js/lib/core')
3 |
4 | hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript'))
5 | hljs.registerLanguage('typescript', require('highlight.js/lib/languages/typescript'))
6 | hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml'))
7 | hljs.registerLanguage('bash', require('highlight.js/lib/languages/bash'))
8 |
9 | function genHtml(str, lang){
10 | if(!lang || null == hljs.getLanguage(lang)) return escapeHtml(str)
11 |
12 | try {
13 | return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value
14 | } catch (e) {
15 | return ''
16 | }
17 | }
18 |
19 | module.exports = function (str, lang) {
20 | return `${genHtml(str, lang)}
`
21 | }
--------------------------------------------------------------------------------
/api-extractor.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3 | "mainEntryPointFilePath": "/packages/core/types/index.d.ts",
4 | "apiReport": {
5 | "enabled": false
6 | },
7 | "docModel": {
8 | "enabled": false
9 | },
10 | "dtsRollup": {
11 | "enabled": true,
12 | "untrimmedFilePath": "/packages/core/dist/index.d.ts"
13 | },
14 | "tsdocMetadata": {
15 | "enabled": false
16 | },
17 | "messages": {
18 | "compilerMessageReporting": {
19 | "default": {
20 | "logLevel": "warning"
21 | }
22 | },
23 | "extractorMessageReporting": {
24 | "default": {
25 | "logLevel": "warning"
26 | }
27 | },
28 | "tsdocMessageReporting": {
29 | "default": {
30 | "logLevel": "warning"
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/checkbox/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import icon from '@common/svg/checkbox.svg'
3 | import { MULTIPLE_OPTION_VALUE_COMPARE } from '@bootstrap/logic'
4 |
5 | import checkbox from './checkbox.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'checkbox',
11 | title: '多选',
12 | setting: setting,
13 | build: checkbox,
14 | validator(field, value: any[]){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | return Promise.resolve()
17 | },
18 | onCreate(field, params, init){
19 | const options = Array.isArray(params.options) ? params.options : []
20 | if(init) {
21 | options.push({ value: '选项1' })
22 | field.attributes.layout = 'inline'
23 | }
24 |
25 | field.options = options
26 | },
27 | logic: [
28 | MULTIPLE_OPTION_VALUE_COMPARE
29 | ]
30 | })
--------------------------------------------------------------------------------
/packages/core/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from 'vue'
2 | import { FormOption } from './model'
3 | import { use } from './api'
4 |
5 | import FormDesigner from './component/FormDesigner/component'
6 | import FormBuilder from './component/FormBuilder/component'
7 | import FormViewer from './component/FormViewer/component'
8 | import FormItem from './component/FormItem/component'
9 |
10 | const version = __VERSION__
11 | const install = function(app: App, options: FormOption){
12 | if(null != options) use(options)
13 |
14 | app.component(FormDesigner.name, FormDesigner)
15 | app.component(FormBuilder.name, FormBuilder)
16 | app.component(FormViewer.name, FormViewer)
17 | app.component(FormItem.name, FormItem)
18 | }
19 |
20 | const xForm = {
21 | install,
22 | version,
23 | }
24 |
25 | export {
26 | install,
27 | version,
28 | }
29 |
30 | export * from './model/Exports'
31 | export * from './api/Exports'
32 | export default xForm
--------------------------------------------------------------------------------
/packages/core/model/Serializable.ts:
--------------------------------------------------------------------------------
1 | import { isObject, toArray } from '../util/lang'
2 |
3 | export class Serializable{
4 | static readonly EXCLUDE_PROPS_KEY = Symbol()
5 |
6 | toJSON(){
7 | const origin = this as any
8 | const ctor = this.constructor as any
9 | const props = toArray(ctor[Serializable.EXCLUDE_PROPS_KEY])
10 | return Object.keys(origin)
11 | .filter(i => props.indexOf(i) < 0)
12 | .reduce((acc, k) => ((acc[k] = getValue(origin[k])), acc), {} as any)
13 | }
14 | }
15 |
16 | function getValue(value: unknown): any{
17 | if(null == value) return value
18 | if(value instanceof Serializable) return value.toJSON()
19 | if(Array.isArray(value)) return value.map(getValue)
20 | if(isObject(value)) {
21 | return Object.keys(value).reduce((acc, key) => {
22 | acc[key] = getValue((value as any)[key])
23 | return acc
24 | }, {} as any)
25 | }
26 |
27 | return value
28 | }
--------------------------------------------------------------------------------
/packages/element-plus/fields/checkbox/index.ts:
--------------------------------------------------------------------------------
1 | import { Field, isEmpty } from '@dongls/xform'
2 | import { MULTIPLE_OPTION_VALUE_COMPARE } from '@element-plus/logic'
3 |
4 | import icon from '@common/svg/checkbox.svg'
5 | import checkbox from './checkbox.vue'
6 | import setting from './setting.vue'
7 |
8 | export default Field.create({
9 | icon: icon,
10 | type: 'checkbox',
11 | title: '多选',
12 | setting: setting,
13 | build: checkbox,
14 | validator(field, value: any[]){
15 | if(field.required && isEmpty(value)) return Promise.reject('必填')
16 | return Promise.resolve()
17 | },
18 | onCreate(field, params, init){
19 | const options = Array.isArray(params.options) ? params.options : []
20 | if(init) {
21 | options.push({ value: '选项1' })
22 | field.attributes.layout = 'inline'
23 | }
24 |
25 | field.options = options
26 | },
27 | logic: [
28 | MULTIPLE_OPTION_VALUE_COMPARE
29 | ]
30 | })
--------------------------------------------------------------------------------
/packages/common/svg/info.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/document/views/example/viewer.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 | 笔记本电脑报修单
17 |
18 |
19 |
20 | {{ scope.field.value ?? schema.viewerPlaceholder }}
21 |
22 |
23 |
24 |
25 |
26 |
37 |
--------------------------------------------------------------------------------
/packages/core/component/FormDesigner/component.module.css:
--------------------------------------------------------------------------------
1 | .fieldOperate{
2 | display: none;
3 | position: absolute;
4 | top: -1px;
5 | right: -1px;
6 | z-index: 10;
7 | }
8 |
9 | :global(.xform-is-selected) > .fieldOperate{
10 | display: block;
11 | }
12 |
13 | :global(.xform-is-silence) .fieldOperate{
14 | display: none !important;
15 | }
16 |
17 |
18 | .fieldOperateButtons{
19 | display: flex;
20 | flex-flow: row nowrap;
21 | }
22 |
23 | .fieldOperateButtons > button{
24 | width: 26px;
25 | height: 26px;
26 | padding: 0;
27 | margin: 0;
28 |
29 | border: none;
30 | background-color: var(--xform-color-primary);
31 | color: #fff;
32 | outline: none;
33 | cursor: pointer;
34 | }
35 |
36 | .fieldOperateButtons :global(.xform-icon-is-svg) svg{
37 | width: 16px;
38 | height: 16px;
39 | fill: #fff;
40 | margin: 0 auto;
41 | }
42 |
43 | .fieldOperateButtons :global(.xform-icon-is-img){
44 | width: 16px;
45 | height: 16px;
46 | }
47 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | xForm在线示例
9 |
10 |
11 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/scripts/webpack/loaders/markdown-loader/index.js:
--------------------------------------------------------------------------------
1 | const { parseQuery } = require('loader-utils')
2 | const md = require('./markdown-it')
3 |
4 | function getOptions(loaderContext) {
5 | const query = loaderContext.query
6 | if (typeof query === 'string' && query !== '') return parseQuery(loaderContext.query)
7 | if (!query || typeof query !== 'object') return {}
8 | return query
9 | }
10 |
11 | module.exports = function (source) {
12 | const options = getOptions(this)
13 | const esModule = typeof options.esModule !== 'undefined' ? options.esModule : true
14 | const prefix = esModule ? 'export default' : 'module.exports ='
15 |
16 | const html = md.render(source)
17 | .replace(/\u2028/g, '\\u2028')
18 | .replace(/\u2029/g, '\\u2029')
19 | .replace(/__VERSION__/g, process.env.RELEASE_VERSION)
20 |
21 | const HAS_HTML_LOADER = this.loaders.some(l => /\/html-loader\//.test(l.path))
22 | return HAS_HTML_LOADER ? html : `${prefix} ${JSON.stringify(html)};`
23 | }
--------------------------------------------------------------------------------
/scripts/webpack/loaders/markdown-loader/md-section.js:
--------------------------------------------------------------------------------
1 | function isH2(token){
2 | return token.type == 'heading_open' && token.tag == 'h2'
3 | }
4 |
5 | function buildSection(part, state){
6 | const open = new state.Token('block', 'section', 1)
7 | open.block = true
8 |
9 | const close = new state.Token('block', 'section', -1)
10 | close.block = true
11 |
12 | part.unshift(open)
13 | part.push(close)
14 | return part
15 | }
16 |
17 | module.exports = function(md){
18 | md.core.ruler.push('section', function(state){
19 | const tokens = state.tokens
20 | const ids = tokens.map((t, i) => isH2(t) ? i : -1).filter(i => i >= 0)
21 | if(ids[0] > 0) ids.unshift(0)
22 | if(ids[ids.length - 1] < ids.length) ids.push(ids.length)
23 |
24 | state.tokens = ids.reduce((acc, item, index, arr) => {
25 | const next = arr[index + 1]
26 | const part = tokens.slice(item, next)
27 | return acc.concat(buildSection(part, state))
28 | }, [])
29 | })
30 | }
--------------------------------------------------------------------------------
/document/component/FooterGuide.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/text/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/common/button.ts:
--------------------------------------------------------------------------------
1 | import { Button, genEventName } from '@dongls/xform'
2 |
3 | import IconTrash from '!!raw-loader!@common/svg/trash.svg'
4 | import { nextTick } from 'vue'
5 |
6 | const EVENT_CLEAR = 'clear'
7 |
8 | export const BUTTON_CLEAR: Button = {
9 | icon: IconTrash,
10 | title: '清空',
11 | handle(field, api, instance){
12 | const name = genEventName(EVENT_CLEAR)
13 | const listener = instance.vnode?.props?.[name]
14 | const useDefault = function(){
15 | const rfs = field.fields.filter(f => f.allowRemove !== false)
16 |
17 | for(const f of rfs){
18 | field.remove(f)
19 |
20 | nextTick(() => {
21 | const hook = field.conf?.onRemoved
22 | if(typeof hook == 'function'){
23 | hook(f, field, instance)
24 | }
25 | })
26 | }
27 |
28 | api.updateSchema()
29 | }
30 |
31 | typeof listener == 'function' ? instance.emit(EVENT_CLEAR, { field, useDefault }): useDefault()
32 | }
33 | }
--------------------------------------------------------------------------------
/document/docs/introduction.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 介绍
4 | **基于[Vue@3.x][vue]的动态表单生成器**,主要用于处理用户自行设计表单的业务场景。这里提供了一个[在线示例][example]更直观的展示该项目,或者可以查看[快速上手][quickstart]了解如何使用。
5 |
6 | ## 特性 ✨
7 | - **可扩展** - 提供完善的字段扩展机制,可自行扩展字段
8 | - **可定制** - 提供多样化的配置,让开发人员完全控制表单的行为
9 | - 包含**表单设计**、**表单生成**、**表单展示**在内的组件库
10 | - 可与任何UI库集成,目前已集成`Bootstrap`
11 | - 基于`typescript`和`Vue@3.x`开发
12 |
13 | ## 架构
14 | 
15 | xForm不提供具体的字段实现,专注于提供灵活的字段扩展机制。通过将底层核心与字段的解耦,具体字段实现可以基于任意UI库,只需要满足xForm的规则即可。
16 |
17 | 考虑到xForm提供的功能与实际需求可能存在不相匹配的情况,因此在设计时就将**可扩展性**作为首要因素。为了使用户可以完全的控制表单的行为,xForm支持以下几种层级的配置:
18 | - 全局配置(`config`) - 表单的默认行为
19 | - 字段配置(`XField`) - 用户控制的行为
20 | - 字段类型配置(`XFieldConf`) - 字段类型的行为
21 | - 组件配置(`slot`) - 具体组件下的行为
22 |
23 | 通常情况下用户并不懂技术,所以xForm让开发人员通过一系列的配置控制表单行为,在屏蔽技术细节的基础上提供自定义能力供用户使用。
24 |
25 | 简单的说,**面向普通用户隐藏技术细节,面向开发人员提供完整的控制力。**
26 |
27 | [example]: https://dongls.github.io/xForm/example.html
28 | [vue]: https://github.com/vuejs/vue-next
29 | [quickstart]: /doc/quickstart
--------------------------------------------------------------------------------
/packages/element-plus/fields/textarea/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/text/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/textarea/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/group/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 允许收起
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/select/select.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
15 |
16 |
17 |
18 |
42 |
43 |
--------------------------------------------------------------------------------
/packages/common/svg/pickup.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/webpack/loaders/markdown-loader/md-container.js:
--------------------------------------------------------------------------------
1 | const Container = require('markdown-it-container')
2 |
3 | const CONTAINER_REG = /^\W*(danger|tip|warning)(?:\s+(.*))?$/
4 | const validate = params => CONTAINER_REG.test(params)
5 |
6 | const CODEBOX_REG = /^code-box\s*(.*)$/
7 |
8 | function render(tokens, idx){
9 | const token = tokens[idx]
10 |
11 | if(token.nesting == 1){
12 | const info = token.info
13 | const result = info.match(CONTAINER_REG)
14 | const type = result[1] || 'tip'
15 | const title = result[2]
16 | const head = title ? `${title}
` : ''
17 | return `${ head }`
18 | }
19 |
20 | return '
'
21 | }
22 |
23 | module.exports = function(md){
24 | md.use(Container, 'alert', { validate, render })
25 | md.use(Container, 'code-box', {
26 | validate(params) {
27 | return params.trim().match(CODEBOX_REG)
28 | },
29 | render(tokens, idx) {
30 | return tokens[idx].nesting === 1 ? '' : ''
31 | }
32 | })
33 | return md
34 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xForm
2 | 基于[Vue@3.2+][vue]的动态表单生成器。[在线示例][doc]
3 |
4 | > `xForm`使用了诸如`expose`、`.prop`之类的api, **请确保你的`Vue`版本不低于3.2.0**。
5 |
6 | ## `v0.9.0`
7 | - [ ] 重构字段逻辑
8 | - [x] 适配`Element Plus@2.2.2`
9 |
10 | ## 兼容性
11 | `xForm`使用了诸如`Proxy`、`Reflect`之类的新特性,因此需要浏览器至少实现了`ES2015`标准。需要注意的是,`xForm`**不支持IE浏览器,也没有相关的支持计划**。
12 |
13 | ## FAQ
14 | ### 文档
15 | `xForm`当前还在开发中,可能会产生各种**不兼容**的情况,等正式版发布时一同发布文档。有关`xForm`的用法可参照在线示例的[源码][example]或者相关[测试用例][test]
16 |
17 | ## 字段库
18 | `xForm`本身并**不提供具体字段类型的实现**,也**不针对特定业务需求提供实现**。考虑到具体业务场景的多样化,`xForm`也无法一一满足这些要求,因此推荐**自行实现字段库**。通过这种方式可以实现
19 | - 使用适合的UI库
20 | - 完全控制每一种字段,满足自身的需求
21 |
22 | 当然这么做需要花费更多的时间,`xForm`提供了以下几个基础的字段库,可以自行参考或直接使用:
23 | - [Element Plus](packages/element-plus)
24 | - [Bootstrap](packages/bootstrap)
25 |
26 | ## License
27 | [MIT](LICENSE)
28 |
29 | [vue]: https://github.com/vuejs/core
30 | [doc]: https://dongls.github.io/xForm/
31 | [example]: https://github.com/dongls/xForm/tree/master/document/views/example
32 | [test]: https://github.com/dongls/xForm/tree/master/packages/core/__test__
33 |
34 | [element]: https://github.com/element-plus/element-plus
--------------------------------------------------------------------------------
/packages/core/api/Store.ts:
--------------------------------------------------------------------------------
1 | import { ComponentOptions } from 'vue'
2 | import { Field, FieldLogic } from '../model/Field'
3 | import { BaseFormConfig } from '../model/common'
4 | import { isEmpty, isNull, genRandomStr } from '../util'
5 |
6 | export interface Preset{
7 | name: string
8 | version?: string
9 | cleanup: void | (() => void)
10 | }
11 |
12 | export function createConfig(): BaseFormConfig {
13 | return {
14 | modes: null,
15 | logic: false,
16 | validation: {
17 | immediate: true
18 | },
19 | genName(){
20 | return `field_${Date.now().toString(36)}_${genRandomStr()}`
21 | },
22 | formatter(field, props){
23 | const value = field.value
24 |
25 | if(isNull(value) || isEmpty(value)) return props.schema.viewerPlaceholder ?? ''
26 | return Array.isArray(value) ? value.join(',') : value
27 | }
28 | }
29 | }
30 |
31 | export const store = {
32 | preset: null as Preset,
33 | config: createConfig(),
34 | fields: new Map(),
35 | slots: new Map(),
36 | logic: new Map()
37 | }
--------------------------------------------------------------------------------
/packages/core/__test__/FormScope.spec.ts:
--------------------------------------------------------------------------------
1 | import { createSchema } from '../index'
2 | import { FormField } from '../model'
3 |
4 | test('form scope', async () => {
5 | const schema = createSchema({})
6 | const field = new FormField()
7 | const field_2 = new FormField()
8 |
9 | expect(schema.root).toBe(schema)
10 | expect(schema.parent).toBe(null)
11 | expect(field.parent).toBeNull()
12 | expect(field.root).toBeNull()
13 |
14 | schema.push(field)
15 | schema.insert(0, field_2)
16 |
17 | expect(field.parent).toBe(schema)
18 | expect(field.root).toBe(schema)
19 | expect(field_2.parent).toBe(schema)
20 | expect(schema.indexOf(field_2)).toBe(0)
21 |
22 | field.move(0)
23 | expect(schema.indexOf(field)).toBe(0)
24 | expect(schema.indexOf(field_2)).toBe(1)
25 |
26 | const subField = new FormField()
27 | field.push(subField)
28 | expect(subField.parent).toBe(field)
29 | expect(subField.root).toBe(schema)
30 |
31 | await Promise.resolve()
32 |
33 | schema.remove(field)
34 | expect(field.parent).toBe(null)
35 | expect(field.root).toBe(null)
36 | expect(subField.root).toBe(null)
37 | })
--------------------------------------------------------------------------------
/packages/element-plus/fields/date/date.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
42 |
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-present dongls
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/bootstrap/logic/index.tsx:
--------------------------------------------------------------------------------
1 | import { registerLogic, removeLogic } from '@dongls/xform'
2 |
3 | import {
4 | STRING_VALUE_COMPARE,
5 | STRING_LENGTH_COMPARE
6 | } from './string'
7 |
8 | import {
9 | NUMBER_VALUE_COMPARE
10 | } from './number'
11 |
12 | import {
13 | DATE_VALUE_COMPARE
14 | } from './date'
15 |
16 | import {
17 | OPTION_VALUE_COMPARE,
18 | MULTIPLE_OPTION_VALUE_COMPARE
19 | } from './option'
20 |
21 | function use(){
22 | registerLogic(STRING_VALUE_COMPARE)
23 | registerLogic(STRING_LENGTH_COMPARE)
24 | registerLogic(NUMBER_VALUE_COMPARE)
25 | registerLogic(DATE_VALUE_COMPARE)
26 | registerLogic(OPTION_VALUE_COMPARE)
27 | registerLogic(MULTIPLE_OPTION_VALUE_COMPARE)
28 | }
29 |
30 | function remove(){
31 | removeLogic(STRING_VALUE_COMPARE)
32 | removeLogic(STRING_LENGTH_COMPARE)
33 | removeLogic(NUMBER_VALUE_COMPARE)
34 | removeLogic(DATE_VALUE_COMPARE)
35 | removeLogic(OPTION_VALUE_COMPARE)
36 | removeLogic(MULTIPLE_OPTION_VALUE_COMPARE)
37 | }
38 |
39 | export * from './string'
40 | export * from './number'
41 | export * from './date'
42 | export * from './option'
43 |
44 | export default {
45 | use,
46 | remove
47 | }
--------------------------------------------------------------------------------
/packages/element-plus/logic/index.ts:
--------------------------------------------------------------------------------
1 | import { registerLogic, removeLogic } from '@dongls/xform'
2 |
3 | import {
4 | STRING_VALUE_COMPARE,
5 | STRING_LENGTH_COMPARE
6 | } from './string'
7 |
8 | import {
9 | NUMBER_VALUE_COMPARE
10 | } from './number'
11 |
12 | import {
13 | DATE_VALUE_COMPARE
14 | } from './date'
15 |
16 | import {
17 | OPTION_VALUE_COMPARE,
18 | MULTIPLE_OPTION_VALUE_COMPARE
19 | } from './option'
20 |
21 | function use(){
22 | registerLogic(STRING_VALUE_COMPARE)
23 | registerLogic(STRING_LENGTH_COMPARE)
24 | registerLogic(NUMBER_VALUE_COMPARE)
25 | registerLogic(DATE_VALUE_COMPARE)
26 | registerLogic(OPTION_VALUE_COMPARE)
27 | registerLogic(MULTIPLE_OPTION_VALUE_COMPARE)
28 | }
29 |
30 | function remove(){
31 | removeLogic(STRING_VALUE_COMPARE)
32 | removeLogic(STRING_LENGTH_COMPARE)
33 | removeLogic(NUMBER_VALUE_COMPARE)
34 | removeLogic(DATE_VALUE_COMPARE)
35 | removeLogic(OPTION_VALUE_COMPARE)
36 | removeLogic(MULTIPLE_OPTION_VALUE_COMPARE)
37 | }
38 |
39 | export * from './string'
40 | export * from './number'
41 | export * from './date'
42 | export * from './option'
43 |
44 | export default {
45 | use,
46 | remove
47 | }
--------------------------------------------------------------------------------
/packages/core/model/action.ts:
--------------------------------------------------------------------------------
1 | import { EnumValidateMode, FormField } from '../model'
2 | import { EnumValidityState } from './constant'
3 |
4 | export type FieldInsertAction = {
5 | type: 'field.insert';
6 | field: FormField;
7 | index: number;
8 | }
9 |
10 | export type FieldRemoveAction = {
11 | type: 'field.remove';
12 | field: FormField;
13 | index: number;
14 | oldParent: FormField;
15 | }
16 |
17 | export type FieldMoveAction = {
18 | type: 'field.move';
19 | field: FormField;
20 | oldIndex: number;
21 | newIndex: number;
22 | }
23 |
24 | export type ValidateAction = {
25 | type: 'validate';
26 | field: FormField;
27 | mode: EnumValidateMode;
28 | callback: (status: boolean, r: any) => void;
29 | }
30 |
31 | export type ValueChangeAction = {
32 | type: 'value.change';
33 | field: FormField;
34 | }
35 |
36 | export type ValidChangeAction = {
37 | type: 'valid.change',
38 | field: FormField,
39 | oldValue: EnumValidityState
40 | newValue: EnumValidityState
41 | }
42 |
43 | export type Action =
44 | FieldInsertAction |
45 | FieldRemoveAction |
46 | FieldMoveAction |
47 | ValidateAction |
48 | ValueChangeAction |
49 | ValidChangeAction
--------------------------------------------------------------------------------
/scripts/example.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 |
4 | const BASE_PATH = path.resolve(__dirname, '../example')
5 | const OUTPUT_BASE_PATH = BASE_PATH + '/packages'
6 |
7 | if(!fs.existsSync(OUTPUT_BASE_PATH)) {
8 | process.env.OUTPUT_BASE_PATH = OUTPUT_BASE_PATH
9 | require('./utils').buildCode()
10 | }
11 |
12 | const files = fs.readdirSync(BASE_PATH).filter(name => name.endsWith('.html'))
13 | const links = files.map(f => `${f}`).join('')
14 | const html = `
15 |
16 |
17 |
18 | res.send(html))
29 |
30 | app.listen(port, () => {
31 | const link = chalk.green.bold(`http://localhost:${port}`)
32 | const arrow = chalk.green.bold('➜')
33 | console.log(arrow + ' example running at ' + link)
34 | })
35 |
--------------------------------------------------------------------------------
/document/router.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router'
2 | import { website } from '@config'
3 |
4 | import Example from './views/example/main.vue'
5 | import Designer from './views/example/designer.vue'
6 | import Builder from './views/example/builder.vue'
7 | import Viewer from './views/example/viewer.vue'
8 |
9 | import { Doc, Page } from './views/document'
10 | import NotFound from './views/not-found.vue'
11 |
12 | const router = createRouter({
13 | history: createWebHashHistory(website.base),
14 | routes: [
15 | { path: '/', redirect: '/example/designer' },
16 | {
17 | path: '/example',
18 | component: Example,
19 | children: [
20 | { path: '', redirect: '/example/designer' },
21 | { path: 'designer', component: Designer },
22 | { path: 'builder', component: Builder },
23 | { path: 'viewer', component: Viewer }
24 | ]
25 | },
26 | {
27 | path: '/doc',
28 | component: Doc,
29 | children: [
30 | { path: '', redirect: '/doc/introduction' },
31 | { path: ':doc', component: Page }
32 | ]
33 | },
34 | { path: '/:catchAll(.*)', component: NotFound }
35 | ]
36 | })
37 |
38 | export default router
--------------------------------------------------------------------------------
/packages/core/model/Emitter.ts:
--------------------------------------------------------------------------------
1 | import { usePrivateProps } from '../util/lang'
2 | type Callbacks = Map>
3 |
4 | const PRIV_PROPS = usePrivateProps<{ callbacks: Callbacks }>()
5 |
6 | export class Emitter{
7 | constructor(){
8 | PRIV_PROPS.create(this, { callbacks: new Map() })
9 | }
10 |
11 | on(type: string, callback: Function){
12 | const callbacks = PRIV_PROPS.get(this, 'callbacks')
13 | if(!callbacks.has(type)){
14 | callbacks.set(type, new Set)
15 | }
16 |
17 | callbacks.get(type).add(callback)
18 | return this
19 | }
20 |
21 | off(type: string, callback: Function){
22 | const callbacks = PRIV_PROPS.get(this, 'callbacks')
23 |
24 | if(callbacks.has(type)){
25 | callbacks.get(type).delete(callback)
26 | }
27 |
28 | return this
29 | }
30 |
31 | trigger(type: string, event?: any){
32 | const callbacks = PRIV_PROPS.get(this, 'callbacks')
33 | if(callbacks.has(type)){
34 | const handles = callbacks.get(type)
35 |
36 | for(const handle of handles){
37 | try {
38 | handle(event)
39 | } catch (error) {
40 | console.error(error)
41 | }
42 | }
43 | }
44 |
45 | return this
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/scripts/args.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | function isEmptyString(value){
4 | return value == null || value.length == 0
5 | }
6 |
7 | process.env.NODE_ENV = process.env.NODE_ENV == 'production' ? 'production' : 'development'
8 | process.env.VUE_VERSION = require('../package.json').devDependencies.vue.slice(1)
9 |
10 | process.argv.slice(2).forEach(item => {
11 | if(/RELEASE_VERSION=/.test(item)) process.env.RELEASE_VERSION = item.split('=')[1]
12 | if(/RELEASE_TARGET=/.test(item)) process.env.RELEASE_TARGET = item.split('=')[1]
13 | })
14 |
15 | if(isEmptyString(process.env.RELEASE_VERSION)){
16 | process.env.RELEASE_VERSION = require('../package.json').version
17 | }
18 |
19 | if(isEmptyString(process.env.OUTPUT_BASE_PATH)){
20 | process.env.OUTPUT_BASE_PATH = path.resolve(__dirname, '../packages')
21 | }
22 |
23 | const IS_PRODUCTION = process.env.NODE_ENV == 'production'
24 | const IS_DEV = process.env.NODE_ENV != 'production'
25 |
26 | module.exports = {
27 | IS_PRODUCTION,
28 | IS_DEV,
29 | NODE_ENV: process.env.NODE_ENV,
30 | TARGET: process.env.TARGET,
31 | RELEASE_VERSION: process.env.RELEASE_VERSION,
32 | RELEASE_PACKAGE: process.env.RELEASE_PACKAGE,
33 | RELEASE_TARGET: process.env.RELEASE_TARGET,
34 | OUTPUT_BASE_PATH: process.env.OUTPUT_BASE_PATH
35 | }
--------------------------------------------------------------------------------
/packages/bootstrap/index.scss:
--------------------------------------------------------------------------------
1 | .xform-setting .custom-control{
2 | line-height: 24px;
3 | user-select: none;
4 | }
5 |
6 | .xform-setting + .xform-setting{
7 | margin-top: 10px;
8 | }
9 |
10 | .xform-setting header{
11 | font-weight: 700;
12 | }
13 |
14 | .xform-bs-empty-tip{
15 | position: absolute;
16 | top: 45%;
17 | left: 50%;
18 | transform: translateX(-50%);
19 | margin: 0;
20 | text-align: center;
21 | color: #9a9a9a;
22 | font-size: 14px;
23 | font-weight: 600;
24 | }
25 |
26 | .xform-bs-setting-option{
27 | display: flex;
28 | flex-flow: row nowrap;
29 | align-items: center;
30 | margin-bottom: 5px;
31 |
32 | .form-control{
33 | width: auto;
34 | flex: 1;
35 | }
36 |
37 | }
38 |
39 | .xform-bs-setting-option > button{
40 | font-size: 14px;
41 | box-shadow: none !important;
42 | padding: 0 5px;
43 | }
44 |
45 | .xform-bs-tabs-setting-title{
46 | position: relative;
47 |
48 | header{
49 | line-height: 20px;
50 | margin-bottom: 5px;
51 | font-weight: 700;
52 | }
53 |
54 | .custom-checkbox{
55 | position: absolute;
56 | top: -2px;
57 | right: 0;
58 | }
59 | }
60 |
61 | .btn-text{
62 | text-decoration: none;
63 | box-shadow: none !important;
64 |
65 | &:hover{
66 | text-decoration: underline;
67 | }
68 | }
--------------------------------------------------------------------------------
/packages/element-plus/fields/group/index.scss:
--------------------------------------------------------------------------------
1 | .xform-preview-group{
2 | &.xform-is-selected > .xform-preview-cover,
3 | &:hover > .xform-preview-cover{
4 | background-color: rgb(253, 213, 138);
5 | }
6 | }
7 |
8 | .xform-el-group{
9 | display: block;
10 |
11 | .el-card{
12 | --el-card-padding: 10px;
13 | }
14 |
15 | .xform-el-group-list{
16 | position: relative;
17 | min-height: 200px;
18 | }
19 |
20 | .el-card__header{
21 | background-color: #f5f7fa;
22 | }
23 |
24 | .xform-el-card-header{
25 | display: flex;
26 | flex-flow: row nowrap;
27 | align-items: center;
28 | line-height: 24px;
29 |
30 | h3{
31 | flex: 1;
32 | margin: 0;
33 | font-size: var(--xform-font-size);
34 | color: var(--xform-text-color);
35 | font-weight: 700;
36 |
37 | overflow: hidden;
38 | text-overflow: ellipsis;
39 | white-space: nowrap;
40 |
41 | cursor: default;
42 | }
43 |
44 | .el-button--text{
45 | padding: 0;
46 | font-size: var(--xform-font-size);
47 | min-height: 0;
48 | }
49 | }
50 |
51 | &.xform-is-collasped{
52 | .el-card__header{
53 | border-bottom: 0;
54 | }
55 |
56 | .el-card__body{
57 | display: none;
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/scripts/webpack/loaders/markdown-loader/markdown-it.js:
--------------------------------------------------------------------------------
1 | require('./extend-html')
2 |
3 | const EMOJI_REG = require('emoji-regex')()
4 | const MarkdownIt = require('markdown-it')
5 | const highlight = require('./highlight')
6 |
7 | const mdAttrs = require('markdown-it-attrs')
8 | const mdContainer = require('./md-container')
9 | const mdSection = require('./md-section')
10 |
11 | const md = new MarkdownIt({ html: true, highlight })
12 | const genId = content => content.replace(/\s+/g, '').replace(EMOJI_REG, '')
13 |
14 | md.use(mdAttrs)
15 | md.use(mdContainer)
16 | md.use(mdSection)
17 |
18 | md.renderer.rules.heading_open = function(tokens, idx, options, env, slf){
19 | const token = tokens[idx]
20 | const next = tokens[idx + 1]
21 |
22 | // 需要包裹一下
23 | if(null != token && token.nesting == 1 && token.tag == 'h2' && null != next){
24 | token.attrJoin('class', 'head-anchor article-sticky-heading')
25 | token.attrSet('id', genId(next.content))
26 | }
27 |
28 | return slf.renderToken(tokens, idx, options)
29 | }
30 |
31 | md.renderer.rules.bullet_list_open = function(tokens, idx, options, env, slf){
32 | const token = tokens[idx]
33 |
34 | if(null != token){
35 | token.attrJoin('class', 'doc-ul')
36 | }
37 |
38 | return slf.renderToken(tokens, idx, options)
39 | }
40 |
41 |
42 | module.exports = md
--------------------------------------------------------------------------------
/packages/element-plus/fields/radio/radio.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
15 |
46 |
47 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/checkbox/checkbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
15 |
46 |
47 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/group/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/packages/core/api/DefaultValue.ts:
--------------------------------------------------------------------------------
1 | import { isFunction, isValidArray } from '../util'
2 | import { FormField, BuiltInDefaultValueType } from '../model'
3 |
4 | type DefaultValueGenerator = (field: FormField) => any
5 |
6 | const generators = new Map()
7 |
8 | export function registerDefaultValueType(type: string, callback: DefaultValueGenerator){
9 | generators.set(type, callback)
10 | }
11 |
12 | export function removeDefaultValueType(type: string){
13 | generators.delete(type)
14 | }
15 |
16 | export function genDefaultValue(field: FormField){
17 | const dv = field.defaultValue
18 | const fn = generators.get(dv?.type)
19 | const gen = isFunction(fn) ? fn : generators.get(BuiltInDefaultValueType.MANUAL)
20 | return gen(field)
21 | }
22 |
23 | const r = registerDefaultValueType
24 | const b = BuiltInDefaultValueType
25 |
26 | r(b.MANUAL, function(field){
27 | return field.defaultValue?.value
28 | })
29 |
30 | r(b.OPTION_ALL, function(field){
31 | const options = field.options
32 | return isValidArray(options) ? options.map(o => o.value) : []
33 | })
34 |
35 | r(b.OPTION_FIRST, function(field){
36 | return field.options?.[0]?.value
37 | })
38 |
39 | r(b.DATE_NOW, function(){
40 | const date = new Date()
41 | const year = date.getFullYear()
42 | const month = (date.getMonth() + 1).toString().padStart(2, '0')
43 | const day = date.getDate().toString().padStart(2, '0')
44 | return `${year}-${month}-${day}`
45 | })
--------------------------------------------------------------------------------
/document/component/Notification/index.scss:
--------------------------------------------------------------------------------
1 | .notification-layout{
2 | position: absolute;
3 | z-index: 10000;
4 | }
5 |
6 | .notification-layout[data-placement="top-right"]{
7 | right: 10px;
8 | }
9 |
10 | .notification{
11 | position: relative;
12 | width: 360px;
13 | padding: 10px;
14 | border-radius: 2px;
15 | background-color: #fff;
16 | box-shadow: 1px 6px 20px 5px rgb(40 120 255 / 13%), 1px 16px 24px 2px rgb(0 0 0 / 8%);
17 |
18 | h3{
19 | margin: 0;
20 | line-height: 20px;
21 | overflow: hidden;
22 | text-overflow: ellipsis;
23 | white-space: nowrap;
24 | padding: 5px 30px 5px 0;
25 | }
26 | }
27 |
28 | .notification-content{
29 | line-height: 20px;
30 | padding: 5px 0;
31 | word-break: break-all;
32 | max-height: 240px;
33 | overflow: auto;
34 | white-space: pre-line;
35 | }
36 |
37 | .notification-close{
38 | position: absolute;
39 | top: 10px;
40 | right: 10px;
41 | border: none;
42 | outline: none;
43 | background-color: transparent;
44 | cursor: pointer;
45 | width: 30px;
46 | height: 30px;
47 | line-height: 30px;
48 | padding: 0;
49 | font-size: 24px;
50 | color: #666;
51 | transition: color ease .3s;
52 | outline: none !important;
53 |
54 | &:hover{
55 | color: red;
56 | }
57 | }
58 |
59 | .notification-error{
60 | .notification-title{
61 | color: #dc3545;
62 | }
63 | }
64 |
65 | .notification-success{
66 | .notification-title{
67 | color: #28a745;
68 | }
69 | }
--------------------------------------------------------------------------------
/packages/element-plus/fields/number/number.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
52 |
53 |
--------------------------------------------------------------------------------
/packages/common/__test__/operator.spec.ts:
--------------------------------------------------------------------------------
1 | import { Operators } from '../operator'
2 |
3 | test('operator: option_eq', () => {
4 | const o = Operators.OPERATOR_OPTION_EQ
5 |
6 | expect(o.test([1, 2, 3], [3, 2, 1])).toBe(true)
7 | expect(o.test([1, 2, 3], [])).toBe(false)
8 | expect(o.test([1, 2, 3], null)).toBe(false)
9 | expect(o.test([1, 2, 3], [3, 1])).toBe(false)
10 | expect(o.test(['1', '2', '3'], [3, 2, 1])).toBe(false)
11 | })
12 |
13 | test('operator: option_ne', () => {
14 | const o = Operators.OPERATOR_OPTION_NE
15 |
16 | expect(o.test([1, 2, 3], [3, 2, 1])).toBe(false)
17 | expect(o.test([1, 2, 3], [])).toBe(true)
18 | expect(o.test([1, 2, 3], null)).toBe(false)
19 | expect(o.test([1, 2, 3], [3, 1])).toBe(true)
20 | expect(o.test(['1', '2', '3'], [3, 2, 1])).toBe(true)
21 | })
22 |
23 | test('operator: option_contains', () => {
24 | const o = Operators.OPERATOR_OPTION_CONTAINS
25 |
26 | expect(o.test([1, 2, 3], [3, 2, 1])).toBe(true)
27 | expect(o.test([1, 2, 3], [])).toBe(true)
28 | expect(o.test([1, 2, 3], null)).toBe(false)
29 | expect(o.test([1, 2, 3], [3, 1])).toBe(true)
30 | expect(o.test(['1', '2', '3'], [3, 2, 1])).toBe(false)
31 | })
32 |
33 | test('operator: option_empty', () => {
34 | const o = Operators.OPERATOR_OPTION_EMPTY
35 |
36 | expect(o.test([1, 2, 3])).toBe(false)
37 | expect(o.test([])).toBe(true)
38 | expect(o.test(null)).toBe(true)
39 | expect(o.test(1)).toBe(true)
40 | expect(o.test([''])).toBe(false)
41 | })
--------------------------------------------------------------------------------
/document/util/enhance.ts:
--------------------------------------------------------------------------------
1 | function parseMeta(article: Element){
2 | const meta = article.querySelector('md-meta')
3 | if(null == meta) return {}
4 |
5 | return {
6 | toc: meta.getAttribute('toc') !== 'false'
7 | }
8 | }
9 |
10 | function getOffsetTop(element: HTMLElement): number {
11 | if(null == element.offsetParent) return 0
12 | return element.offsetTop + getOffsetTop(element.offsetParent as HTMLElement)
13 | }
14 |
15 | export function scrollTo(selector: string){
16 | try {
17 | const head = document.querySelector(decodeURIComponent(selector)) as HTMLElement
18 | if(null == head) return
19 |
20 | setTimeout(() => document.documentElement.scrollTop = getOffsetTop(head), 0)
21 | } catch (error) { /**/ }
22 | }
23 |
24 |
25 | function renderToc(article: Element, meta: any){
26 | const toc = article.parentNode.querySelector('.article-toc')
27 |
28 | toc.innerHTML = meta.toc === false ? '' : [...article.querySelectorAll('h2.head-anchor')].map(head => {
29 | const id = head.id
30 | return `${id}`
31 | }).join('')
32 | }
33 |
34 | function enhance(path: string){
35 | const article = document.querySelector(`article[path="${path}"]`)
36 | const meta = parseMeta(article)
37 |
38 | renderToc(article, meta)
39 | }
40 |
41 | export default function(path: string, hash: string){
42 | return new Promise(() => {
43 | setTimeout(() => {
44 | enhance(path)
45 | if(hash) scrollTo(hash)
46 | }, 0)
47 | })
48 | }
--------------------------------------------------------------------------------
/packages/bootstrap/fields/number/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
44 |
--------------------------------------------------------------------------------
/document/docs/components/XFormViewer.md:
--------------------------------------------------------------------------------
1 | # XFormViewer 表单查看器
2 | 该组件主要用于根据`XFormDesigner`生成的表单配置展示`XFormBuilder`生成的表单数据,当然你也可以自行编写组件展示表单数据。
3 |
4 | ## 基本用法
5 | ```html
6 |
7 |
8 |
9 | name值为demo的字段会这么显示
10 |
11 |
12 | 所有type为text的字段会这么显示
13 |
14 |
15 |
16 |
17 |
30 | ```
31 |
32 | ## Props
33 | ### schema
34 | - **类型**:`XFormSchema`
35 | - **说明**:表单的配置,数据来自表单设计器,**必须提供**。
36 | ### model
37 | - **类型**:`object`
38 | - **说明**:表单的数据,数据来自表单生成器,**必须提供**。
39 |
40 | ## Slots
41 | ### name_[[name]]
42 | 根据`name`的值定制某一个字段该如何显示,例如:
43 | ```html
44 |
45 |
46 | name值为demo的字段会这么显示
47 |
48 |
49 | ```
50 | ### type_[[type]]
51 | 根据`type`的值定制某一类型的字段该如何显示,例如:
52 | ```html
53 |
54 |
55 | 所有type为text的字段会这么显示
56 |
57 |
58 | ```
--------------------------------------------------------------------------------
/example/bootstrap.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | bootstrap example
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
45 |
46 |
--------------------------------------------------------------------------------
/packages/common/svg/checkbox.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/number/index.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue'
2 | import { Field, FormField, isEmpty } from '@dongls/xform'
3 | import { useValue } from '@bootstrap/util'
4 | import { NUMBER_VALUE_COMPARE } from '@bootstrap/logic'
5 |
6 | import icon from '@common/svg/number.svg'
7 | import setting from './setting.vue'
8 |
9 | const build = defineComponent({
10 | name: 'xform-bs-number',
11 | props: {
12 | field: {
13 | type: FormField,
14 | required: true
15 | },
16 | disabled: {
17 | type: Boolean,
18 | default: false
19 | }
20 | },
21 | setup(props){
22 | const value = useValue()
23 | return function(){
24 | return (
25 |
33 | )
34 | }
35 | }
36 | })
37 |
38 | export default Field.create({
39 | icon,
40 | type: 'number',
41 | title: '数字',
42 | setting,
43 | build,
44 | validator(field, value: number | string){
45 | if(field.required && isEmpty(value)) return Promise.reject('必填')
46 | if(field.attributes.integer && !/^[-+]?[1-9]?\d+$/.test(value + '')) return Promise.reject('请输入整数')
47 |
48 | return Promise.resolve()
49 | },
50 | logic: [
51 | NUMBER_VALUE_COMPARE
52 | ]
53 | })
--------------------------------------------------------------------------------
/packages/bootstrap/fields/group/index.scss:
--------------------------------------------------------------------------------
1 | .xform-preview-group{
2 | &.xform-is-selected > .xform-preview-cover,
3 | &:hover > .xform-preview-cover{
4 | background-color: rgb(253, 213, 138);
5 | }
6 |
7 | .card-body.xform-bs-group-list{
8 | padding: 5px;
9 | }
10 |
11 | .xform-is-top{
12 | padding-left: 0 !important;
13 | }
14 | }
15 |
16 | .xform-bs-group{
17 | display: block;
18 |
19 | .xform-is-top{
20 | padding-left: 10px;
21 | }
22 |
23 | .xform-bs-group-list{
24 | position: relative;
25 | min-height: 200px;
26 | padding: 10px 10px 10px 5px;
27 | }
28 | }
29 |
30 | .xform-bs-group.xform-is-collasped{
31 | .card-header{
32 | border-bottom: 0;
33 | }
34 |
35 | .xform-bs-group-list{
36 | display: none;
37 | }
38 | }
39 |
40 | .xform-bs-group .card-header{
41 | display: flex;
42 | flex-flow: row nowrap;
43 | padding-left: 10px;
44 | padding-right: 10px;
45 | font-size: 14px;
46 | font-weight: 700;
47 | align-items: center;
48 | margin-top: 0;
49 |
50 | span{
51 | flex: 1;
52 | overflow: hidden;
53 | text-overflow: ellipsis;
54 | white-space: nowrap;
55 | cursor: default;
56 | font-size: var(--xform-font-size);
57 | color: var(--xform-text-color);
58 | line-height: 20px;
59 | }
60 | }
61 |
62 | .xform-bs-group-toggle{
63 | border: none;
64 | margin: 0;
65 | background-color: transparent;
66 | padding: 0;
67 | height: 16px;
68 | line-height: 16px;
69 | font-size: 14px;
70 | box-shadow: none !important;
71 | }
72 |
--------------------------------------------------------------------------------
/packages/common/svg/number.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/document/views/document/menus.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from 'vue'
2 | import data, { MenuRaw } from '../../docs/index'
3 |
4 | export enum MenuStatusEnum{
5 | INIT = 0,
6 | LOADING = 1,
7 | LOADED = 2
8 | }
9 |
10 | const MENU_RAW_MAP = data.reduce((acc, raw) => {
11 | if(raw.group !== true) acc.set(raw.path, raw)
12 | return acc
13 | }, new Map())
14 |
15 | const MENUS_DATA = data.map(i => {
16 | return {
17 | name: i.name,
18 | group: i.group === true,
19 | path: i.path,
20 | subtitle: i.subtitle,
21 | status: MenuStatusEnum.INIT,
22 | hidden: i.hidden === true
23 | }
24 | })
25 |
26 | const PURE_MENU = MENUS_DATA.filter(i => i.group !== true && i.hidden !== true)
27 | export const menus = reactive(MENUS_DATA.filter(i => i.hidden !== true))
28 |
29 | export function load(path: string){
30 | const raw = MENU_RAW_MAP.get(path)
31 | if(null == raw) return Promise.resolve()
32 |
33 | const fn = raw.document
34 | return typeof fn == 'function' ? fn().then(m => m.default) : Promise.resolve()
35 | }
36 |
37 | export function getMenu(path: string){
38 | return menus.find(m => m.path == path)
39 | }
40 |
41 | export function getMenuRelation(path: string){
42 | const index = PURE_MENU.findIndex(m => m.path == path)
43 | const max = PURE_MENU.length - 1
44 | if(index < 0 || index > max) return null
45 |
46 | return {
47 | prev: index == 0 ? null : PURE_MENU[index - 1],
48 | next: index == max ? null : PURE_MENU[index + 1]
49 | }
50 | }
51 |
52 | export default {
53 | menus,
54 | load,
55 | getMenu
56 | }
57 |
--------------------------------------------------------------------------------
/packages/common/svg/pick.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/core/__test__/FormField.spec.ts:
--------------------------------------------------------------------------------
1 | import { FormField } from '../model'
2 | import { getPrivateProps } from '../util/lang'
3 |
4 | test('FormField: rest params', () => {
5 | const o = {
6 | name: 'field_a',
7 | type: 'text',
8 | otherProp: 'other prop'
9 | }
10 |
11 | const field = new FormField(o)
12 | expect(field.name).toBe(o.name)
13 | expect(field.type).toBe(o.type)
14 | expect(field.otherProp).toBe(o.otherProp)
15 | })
16 |
17 | test('FormField: clone', () => {
18 | const origin = new FormField({
19 | name: 'origin',
20 | type: 'text',
21 | attributes: {
22 | test: 1
23 | }
24 | })
25 |
26 | const child_1 = new FormField({
27 | name: 'child_1',
28 | type: 'text'
29 | })
30 |
31 | const child_2 = new FormField({
32 | name: 'child_2',
33 | type: 'textarea',
34 | allowClone: false
35 | })
36 |
37 | origin.fields.push(child_1)
38 | origin.fields.push(child_2)
39 |
40 | const clone = origin.clone()
41 |
42 | expect(clone).not.toBe(origin)
43 | expect(clone.name).not.toBe(origin.name)
44 | expect(clone.attributes.test).toBe(origin.attributes.test)
45 | expect(clone.fields).toBeInstanceOf(Array)
46 | expect(clone.fields.length).toBe(1)
47 | expect(clone.fields[0].name).not.toBe(origin.fields[0].name)
48 | expect(clone.fields[0].type).toBe(origin.fields[0].type)
49 | })
50 |
51 | test('FormField: private props', () => {
52 | const field = new FormField({
53 | name: 'field_a',
54 | type: 'text',
55 | })
56 |
57 | expect(() => getPrivateProps(field, Symbol())).toThrow()
58 | })
59 |
60 |
--------------------------------------------------------------------------------
/packages/common/svg/group.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/common/svg/divider.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/element.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | element-plus example
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
47 |
48 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/select/select.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
53 |
54 |
--------------------------------------------------------------------------------
/packages/element-plus/FormSetting.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
37 |
41 |
49 |
--------------------------------------------------------------------------------
/packages/core/api/Logic/logic.ts:
--------------------------------------------------------------------------------
1 | import { notNull } from '../../util/lang'
2 | import { FormField, FormFieldLogic } from '../../model/FormField'
3 | import { FieldLogic } from '../../model/Field'
4 | import { store } from '../Store'
5 |
6 | export function registerLogic(logic: FieldLogic){
7 | store.logic.set(logic.type, logic)
8 | }
9 |
10 | export function removeLogic(logic: string | FieldLogic){
11 | const type = typeof logic == 'string' ? logic : logic.type
12 | const exist = store.logic.get(type)
13 |
14 | if(typeof logic == 'string'){
15 | store.logic.delete(type)
16 | return exist
17 | }
18 |
19 | if(exist == logic){
20 | store.fields.delete(type)
21 | return exist
22 | }
23 |
24 | return null
25 | }
26 |
27 | export function resetLogic(){
28 | store.logic.clear()
29 | }
30 |
31 | export function getLogic(type: string){
32 | return store.logic.get(type)
33 | }
34 |
35 | export function getLogics(types: null | string[]){
36 | if(types == null) return Array.from(store.logic.values()).sort((a, b) => {
37 | const ao = a.order ?? 0
38 | const bo = b.order ?? 0
39 | return ao - bo
40 | })
41 |
42 | return types.map(key => store.logic.get(key)).filter(notNull)
43 | }
44 |
45 | export function getComposedLogic(){
46 | return Array.from(store.logic.values()).filter(l => l.composed === true)
47 | }
48 |
49 | // TODO: 判断字段位置, 只接收filed一个单数,不再接收model
50 | export function test(fieldLogic: FormFieldLogic, field: FormField){
51 | if(null == fieldLogic) return true
52 |
53 | const logic = getLogic(fieldLogic.type)
54 | if(null == logic) return false
55 |
56 | return typeof logic.test == 'function' ? logic.test(fieldLogic, field) : false
57 | }
--------------------------------------------------------------------------------
/packages/common/svg/clone.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/document/docs/index.ts:
--------------------------------------------------------------------------------
1 | export interface MenuRaw {
2 | name: string;
3 | path?: string;
4 | group?: boolean;
5 | subtitle?: string;
6 | document?: () => Promise;
7 | hidden: boolean;
8 | }
9 |
10 | export default [
11 | {
12 | name: '基础',
13 | group: true
14 | },
15 | {
16 | name: '简介',
17 | path: '/doc/introduction',
18 | document: () => import('./introduction.md')
19 | },
20 | {
21 | name: '快速上手',
22 | path: '/doc/quickstart',
23 | document: () => import('./quickstart.md')
24 | },
25 | // {
26 | // name: '概念',
27 | // path: '/doc/concept',
28 | // document: () => import('./concept.md'),
29 | // hidden: true
30 | // },
31 | // {
32 | // name: '组件',
33 | // group: true
34 | // },
35 | // {
36 | // name: 'XFormDesigner',
37 | // path: '/doc/XFormDesigner',
38 | // subtitle: '表单设计器',
39 | // document: () => import('./components/XFormDesigner.md')
40 | // },
41 | // {
42 | // name: 'XFormBuilder',
43 | // path: '/doc/XFormBuilder',
44 | // subtitle: '表单生成器',
45 | // document: () => import('./components/XFormBuilder.md')
46 | // },
47 | // {
48 | // name: 'XFormViewer',
49 | // path: '/doc/XFormViewer',
50 | // subtitle: '表单查看器',
51 | // document: () => import('./components/XFormViewer.md')
52 | // },
53 | // {
54 | // name: 'XFormItem',
55 | // path: '/doc/XFormItem',
56 | // subtitle: '表单项',
57 | // document: () => import('./components/XFormItem.md')
58 | // },
59 | // {
60 | // name: '其他',
61 | // group: true
62 | // },
63 | // {
64 | // name: '类型定义',
65 | // path: '/doc/model',
66 | // document: () => import('./model.md')
67 | // },
68 | ] as MenuRaw[];
--------------------------------------------------------------------------------
/docs/55.9b82bab6.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkxform=self.webpackChunkxform||[]).push([[55],{2055:(o,e,t)=>{t.r(e),t.d(e,{default:()=>r});var s=t(7091),c=t.n(s),i=new URL(t(3877),t.b);const r=' 介绍
基于Vue@3.x的动态表单生成器,主要用于处理用户自行设计表单的业务场景。这里提供了一个在线示例更直观的展示该项目,或者可以查看快速上手了解如何使用。
特性 ✨
- 可扩展 - 提供完善的字段扩展机制,可自行扩展字段
- 可定制 - 提供多样化的配置,让开发人员完全控制表单的行为
- 包含表单设计、表单生成、表单展示在内的组件库
- 可与任何UI库集成,目前已集成
Bootstrap - 基于
typescript和Vue@3.x开发
架构
xForm不提供具体的字段实现,专注于提供灵活的字段扩展机制。通过将底层核心与字段的解耦,具体字段实现可以基于任意UI库,只需要满足xForm的规则即可。
考虑到xForm提供的功能与实际需求可能存在不相匹配的情况,因此在设计时就将可扩展性作为首要因素。为了使用户可以完全的控制表单的行为,xForm支持以下几种层级的配置:
- 全局配置(
config) - 表单的默认行为 - 字段配置(
XField) - 用户控制的行为 - 字段类型配置(
XFieldConf) - 字段类型的行为 - 组件配置(
slot) - 具体组件下的行为
通常情况下用户并不懂技术,所以xForm让开发人员通过一系列的配置控制表单行为,在屏蔽技术细节的基础上提供自定义能力供用户使用。
简单的说,面向普通用户隐藏技术细节,面向开发人员提供完整的控制力。
'},7091:o=>{o.exports=function(o,e){return e||(e={}),o?(o=String(o.__esModule?o.default:o),e.hash&&(o+=e.hash),e.maybeNeedQuotes&&/[\t\n\f\r "'=<>`]/.test(o)?'"'.concat(o,'"'):o):o}},3877:(o,e,t)=>{o.exports=t.p+"ac762befe393daba8318.png"}}]);
--------------------------------------------------------------------------------
/packages/bootstrap/fields/radio/radio.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
47 |
48 |
--------------------------------------------------------------------------------
/packages/bootstrap/fields/checkbox/checkbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
47 |
48 |
--------------------------------------------------------------------------------
/document/docs/model.md:
--------------------------------------------------------------------------------
1 | ## XField
2 | `XField`主要用于描述字段数据,保存用户对字段的配置。从数据库中取出的数据需要转成该类型才可被组件接受。
3 | ```typescript
4 | interface XField{
5 | type: string; // 字段类型
6 | name: string; // 字段名,建议唯一
7 | title?: string; // 字段标题
8 | placeholder?: string; // 是否必填
9 | required?: boolean; // 选项
10 | options?: any[];
11 |
12 | attributes?: { // 自定义属性,可自行添加所需属性
13 | [prop: string]: any;
14 | remove?: boolean; // 是否允许字段被删除
15 | };
16 |
17 | // 字段缓存,不可枚举
18 | storage: {
19 | fieldConf: XFieldConf;
20 | }
21 |
22 | // 查询字段对应的字段配置
23 | findFieldConf(): XFieldConf | null;
24 | // 复制该字段,name属性除外
25 | copy(): XField;
26 | }
27 | ```
28 |
29 | ## XFieldConf
30 | `XFieldConf`主要用于描述字段类型的配置,例如字段该如何渲染、验证等。`Ref`和`ComponentOptions`都是由`Vue`提供的类型。
31 | ```typescript
32 | type ModeCompontFunc = (field: XField, mode: string) => ComponentOptions | VNode;
33 | type XFieldConfComponent = ComponentOptions | ModeCompontFunc;
34 |
35 | interface XFieldConf {
36 | type: string; // 字段类型
37 | title: string; // 字段名
38 | icon?: string | Function; // 字段icon
39 | custom?: boolean;
40 | attributes?: object | Function;
41 | extension?: object;
42 |
43 | setting?: XFieldConfComponent;
44 | preview?: XFieldConfComponent;
45 | build?: XFieldConfComponent;
46 | view?: XFieldConfComponent
47 |
48 | // 字段验证器
49 | validator?: (
50 | field: XField,
51 | value: any,
52 | model: any,
53 | context: { validating: Ref, message: Ref}
54 | ) => Promise;
55 | }
56 | ```
57 |
58 | ## XFormSchema
59 | `XFormSchema`主要用于描述表单配置数据。数据来源于表单设计器,可序列化后存入数据库。`xForm`就是依据它渲染表单。
60 | ```typescript
61 | interface XFormSchema {
62 | [propName: string]: any;
63 | fields: XField[];
64 | labelSuffix?: string;
65 | labelPosition?: string;
66 | viewerPlaceholder?: string;
67 | }
68 | ```
--------------------------------------------------------------------------------
/packages/global.d.ts:
--------------------------------------------------------------------------------
1 | declare var __VERSION__: string
2 | declare var __IS_DEV__: boolean
3 | declare var __VUE_VERSION__: string
4 | declare var __IS_TEST__: boolean
5 | declare var __TIMESTAMP__: string
6 |
7 | declare module '@config'{
8 | interface config{
9 | base: string
10 | }
11 |
12 | export const website: config
13 | }
14 |
15 | interface Document {
16 | msElementsFromPoint(x: number, y: number): Element[];
17 | }
18 |
19 | declare module '*.module.css' {
20 | const classes: { readonly [key: string]: string }
21 | export default classes
22 | }
23 |
24 | declare module '*.module.scss' {
25 | const classes: { readonly [key: string]: string }
26 | export default classes
27 | }
28 |
29 | declare module '*.module.sass' {
30 | const classes: { readonly [key: string]: string }
31 | export default classes
32 | }
33 |
34 | declare module '*.bmp' {
35 | const src: string
36 | export default src
37 | }
38 |
39 | declare module '*.gif' {
40 | const src: string
41 | export default src
42 | }
43 |
44 | declare module '*.jpg' {
45 | const src: string
46 | export default src
47 | }
48 |
49 | declare module '*.jpeg' {
50 | const src: string
51 | export default src
52 | }
53 |
54 | declare module '*.png' {
55 | const src: string
56 | export default src
57 | }
58 |
59 | declare module '*.webp' {
60 | const src: string
61 | export default src
62 | }
63 |
64 | declare module '*.svg' {
65 | const src: string
66 | export default src
67 | }
68 |
69 | declare module '*.md' {
70 | const src: string
71 | export default src
72 | }
73 |
74 | declare module '*.css' {
75 | const src: string
76 | export default src
77 | }
78 |
79 | declare module '*.vue' {
80 | import type { DefineComponent } from 'vue'
81 | const component: DefineComponent<{}, {}, any>
82 | export default component
83 | }
84 |
--------------------------------------------------------------------------------
/example/bootstrap.esm.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | bootstrap example for esm
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
33 |
34 |
55 |
56 |
--------------------------------------------------------------------------------
/packages/core/__test__/lang.spec.ts:
--------------------------------------------------------------------------------
1 | import { markRaw } from 'vue'
2 | import {
3 | clonePlainObject,
4 | mergePlainObject,
5 | flat,
6 | isEmpty
7 | } from '../util/lang'
8 |
9 | test('lang: clonePlainObject', () => {
10 | const origin = {
11 | a: 1,
12 | b: false,
13 | c: 'abc',
14 | d: null,
15 | e: undefined,
16 | f: function(){/* */},
17 | g: { a: 1, b: 'abc' },
18 | h: [1, 2, 3, 4],
19 | i: [{ a: 1 }, { a: 1 }],
20 | j: markRaw({})
21 | } as any
22 |
23 | const clone = clonePlainObject(origin)
24 |
25 | expect(origin.g).not.toBe(clone.g)
26 | expect(origin.f).toBe(clone.f)
27 | expect(origin).toStrictEqual(clone)
28 | expect(origin.j).toBe(clone.j)
29 | })
30 |
31 | test('lang: mergePlainObject', () => {
32 | const x = { a: 1, b: false, e: function(){/* */}, f: [1, 3], g: 1 }
33 | const y = { b: 2, e: function(){/* */}, g: undefined } as any
34 | const z = { a: 0, c: 'c', b: null, f: [1, 2, 3] } as any
35 | const o = mergePlainObject({}, x, y, z)
36 |
37 | expect(o).not.toBe(x)
38 | expect(o.a).toBe(z.a)
39 | expect(o.b).toBe(z.b)
40 | expect(o.e).toBe(y.e)
41 | expect(o.f).toBe(z.f)
42 | expect(o.g).toBe(x.g)
43 | })
44 |
45 | test('lang: flat', () => {
46 | expect(flat(null)).toStrictEqual([])
47 | expect(flat([1, 2, 3])).toStrictEqual([1, 2, 3])
48 | expect(flat([1, [2, 3]])).toStrictEqual([1, 2, 3])
49 | expect(flat([1, [2], [[3]]])).toStrictEqual([1, 2, [3]])
50 | })
51 |
52 | test('lang: isEmpty', () => {
53 | expect(isEmpty(null)).toBe(true)
54 | expect(isEmpty(undefined)).toBe(true)
55 | expect(isEmpty(' ')).toBe(true)
56 | expect(isEmpty([])).toBe(true)
57 | expect(isEmpty(NaN)).toBe(true)
58 | expect(isEmpty(Infinity)).toBe(true)
59 | expect(isEmpty(-Infinity)).toBe(true)
60 | expect(isEmpty(0)).toBe(false)
61 | expect(isEmpty({})).toBe(false)
62 | })
--------------------------------------------------------------------------------
/document/native/codebox/index.ts:
--------------------------------------------------------------------------------
1 | import css from '!!raw-loader!postcss-loader!./index.css'
2 | import codegen from './codegen'
3 |
4 | function goCodepen(code: string, event: Event){
5 | const data = codegen(code)
6 |
7 | // https://blog.codepen.io/documentation/prefill/
8 | const form = document.createElement('form')
9 | form.method = 'POST'
10 | form.action = 'https://codepen.io/pen/define/'
11 | form.target = '_blank'
12 | form.style.display = 'none'
13 |
14 | const input = document.createElement('input')
15 | input.name = 'data'
16 | input.type = 'hidden'
17 | input.value = JSON.stringify(data)
18 |
19 | form.appendChild(input)
20 | document.body.appendChild(form)
21 |
22 | form.submit()
23 | setTimeout(() => form.remove(), 150)
24 | }
25 |
26 | function createOnline(code: string){
27 | const online = document.createElement('button')
28 | online.type = 'text'
29 | online.textContent = '在线运行'
30 |
31 | online.addEventListener('click', goCodepen.bind(null, code))
32 |
33 | return online
34 | }
35 |
36 | class CodeBox extends HTMLElement {
37 | constructor(){
38 | super()
39 |
40 | const code = this.querySelector('pre.hljs')
41 | const style = document.createElement('style')
42 | style.textContent = css
43 |
44 | const online = createOnline(code.textContent)
45 |
46 | const toolbox = document.createElement('div')
47 | toolbox.className = 'toolbox'
48 | toolbox.appendChild(online)
49 |
50 | const root = document.createElement('div')
51 | root.className = 'code-box-root'
52 | root.appendChild(code)
53 | root.appendChild(toolbox)
54 |
55 | const shadow = this.attachShadow({ mode: 'open' })
56 | shadow.appendChild(style)
57 | shadow.appendChild(root)
58 | }
59 | }
60 |
61 |
62 | export default {
63 | install(){
64 | customElements.define('code-box', CodeBox)
65 | }
66 | }
--------------------------------------------------------------------------------
/packages/core/__test__/api.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createSchemaRef,
3 | findModeGroup,
4 | getConfig,
5 | getPreset,
6 | reset,
7 | resetConfig,
8 | resetPreset,
9 | } from '../api'
10 |
11 | import { FormField, FormSchema } from '../model'
12 | import { isRef } from 'vue'
13 |
14 | describe('createShema', () => {
15 | test('edge cases', () => {
16 | const params = [null, undefined, 1, '', 'abc', false, true, NaN]
17 | for (const param of params) {
18 | const schema = createSchemaRef(param)
19 | expect(isRef(schema)).toBeTruthy()
20 | expect(schema.value).toBeInstanceOf(FormSchema)
21 | expect(schema.value.fields).toBeInstanceOf(Array)
22 | expect(schema.value.fields.length).toBe(0)
23 | }
24 | })
25 |
26 | test('passing data', () => {
27 | const schema = createSchemaRef({
28 | labelSuffix: ':',
29 | fields: [
30 | { type: 'text', name: 'a', title: 'a' },
31 | { type: 'select', name: 'b', title: 'b' },
32 | ],
33 | test: '1',
34 | })
35 |
36 | expect(isRef(schema)).toBeTruthy()
37 | expect(schema.value.labelSuffix).toBe(':')
38 | expect(schema.value.test).toBe('1')
39 |
40 | expect(schema.value.fields).toBeInstanceOf(Array)
41 | expect(schema.value.fields[0]).toBeInstanceOf(FormField)
42 | })
43 | })
44 |
45 | describe('reset', () => {
46 | test('resetConfig', () => {
47 | const o = getConfig()
48 | resetConfig()
49 | const n = getConfig()
50 | expect(o).not.toBe(n)
51 | })
52 |
53 | test('resetPreset', () => {
54 | resetPreset()
55 | expect(getPreset()).toBeNull()
56 | })
57 |
58 | test('reset', () => {
59 | const o = getConfig()
60 | reset()
61 | const n = getConfig()
62 | const fg = findModeGroup()
63 |
64 | expect(o).not.toBe(n)
65 | expect(getPreset()).toBeNull()
66 | expect(fg.length).toBe(0)
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/tabs/index.scss:
--------------------------------------------------------------------------------
1 | .xform-el-tabs{
2 | display: block;
3 | position: relative;
4 | background-color: #fff;
5 |
6 | .el-tabs{
7 | box-shadow: none;
8 | transition: var(--el-transition-duration);
9 | border-radius: 4px;
10 | overflow: hidden;
11 |
12 | &:hover{
13 | box-shadow: var(--el-box-shadow-light);
14 | }
15 | }
16 |
17 | .el-tabs__nav-prev,
18 | .el-tabs__nav-next{
19 | width: 20px;
20 | height: 38px;
21 | text-align: center;
22 |
23 | &:hover{
24 | color: var(--el-color-primary);
25 | }
26 | }
27 |
28 | .el-tabs__content{
29 | padding: 10px;
30 | }
31 |
32 | &.xform-is-with-title {
33 | .el-tabs__header{
34 | display: flex;
35 |
36 | &::before{
37 | content: var(--xform-el-tabs-field-title);
38 | align-self: center;
39 | padding: 0 10px;
40 | font-size: var(--xform-font-size);
41 | color: var(--xform-text-color);
42 | font-weight: 700;
43 | max-width: var(--xform-label-width);
44 | overflow: hidden;
45 | text-overflow: ellipsis;
46 | white-space: nowrap;
47 | box-sizing: content-box;
48 | }
49 | }
50 |
51 | .el-tabs__nav-wrap{
52 | flex: 1;
53 | }
54 |
55 | .el-tabs--border-card .el-tabs__item:first-child{
56 | margin-left: 0;
57 | }
58 | }
59 | }
60 |
61 | .xform-el-tab-pane{
62 | min-height: 200px;
63 | }
64 |
65 | .xform-preview-tabs{
66 | .el-tabs__nav-wrap{
67 | position: relative;
68 | z-index: 9;
69 | }
70 |
71 | .xform-el-tab-pane{
72 | position: relative;
73 | }
74 | }
75 |
76 | .xform-preview-tabs.xform-is-selected > .xform-preview-cover,
77 | .xform-preview-tabs:hover > .xform-preview-cover{
78 | background-color: rgb(253, 213, 138);
79 | }
80 |
81 | .xform-el-tabs-option .el-button--small{
82 | padding-left: 10px;
83 | padding-right: 10px;
84 | }
--------------------------------------------------------------------------------
/packages/bootstrap/fields/tabs/index.scss:
--------------------------------------------------------------------------------
1 | .xform-bs-tabs{
2 | display: block;
3 | background-color: #fff;
4 | position: relative;
5 |
6 | .tab-pane{
7 | padding: 10px 5px;
8 | border-left: 1px solid #dee2e6;
9 | border-right: 1px solid #dee2e6;
10 | border-bottom: 1px solid #dee2e6;
11 | min-height: 200px;
12 | }
13 |
14 | .nav-tabs-title{
15 | padding: .5rem 10px;
16 | border-bottom: 1px solid #dee2e6;
17 | cursor: default;
18 | max-width: var(--xform-label-width);
19 | font-size: var(--xform-font-size);
20 | color: var(--xform-text-color);
21 | overflow: hidden;
22 | text-overflow: ellipsis;
23 | white-space: nowrap;
24 | box-sizing: content-box;
25 | }
26 |
27 | .nav-tabs{
28 | display: flex;
29 | flex-flow: row nowrap;
30 | border-bottom: none;
31 | border: 1px solid #dee2e6;
32 | padding-top: 5px;
33 | border-bottom: none;
34 | background-color: rgba(0,0,0,.03);
35 | border-radius: 0.25rem 0.25rem 0 0;
36 | }
37 |
38 | .xform-bs-tabs-scroll{
39 | flex: 1;
40 | overflow: hidden;
41 | }
42 |
43 | .xform-bs-tab-list{
44 | float: left;
45 | padding: 0 5px;
46 | min-width: 100%;
47 | white-space: nowrap;
48 | border-bottom: 1px solid #dee2e6;
49 |
50 | .nav-link{
51 | display: inline-block;
52 | }
53 | }
54 |
55 | .nav-link{
56 | color: inherit;
57 |
58 | &:hover,
59 | &.active{
60 | color: #007bff;
61 | }
62 | }
63 |
64 | .tab-pane{
65 | border-radius: 0 0 0.25rem 0.25rem;
66 | }
67 | }
68 |
69 | .xform-preview-tabs{
70 | .xform-bs-tabs-scroll{
71 | position: relative;
72 | z-index: 9;
73 | }
74 |
75 | .tab-pane{
76 | position: relative;
77 | padding: 5px;
78 | }
79 | }
80 |
81 | .xform-preview-tabs.xform-is-selected > .xform-preview-cover,
82 | .xform-preview-tabs:hover > .xform-preview-cover{
83 | background-color: rgb(253, 213, 138);
84 | }
--------------------------------------------------------------------------------
/packages/common/svg/trash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/element.esm.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | element-plus example for esm
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
34 |
35 |
59 |
60 |
--------------------------------------------------------------------------------
/packages/common/svg/datatable.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/common/svg/date.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/bootstrap/FormSetting.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
36 |
40 |
48 |
--------------------------------------------------------------------------------
/packages/core/model/Button.ts:
--------------------------------------------------------------------------------
1 | import { nextTick } from 'vue'
2 |
3 | import { EVENTS, SELECTOR } from './constant'
4 | import { genEventName } from '../util/component'
5 | import { isFunction } from '../util/lang'
6 | import { getField } from '../util/dom'
7 |
8 | import { Button } from './common'
9 | import { IconClone, IconRemove, IconPickUp } from '@common/svg/raw'
10 |
11 | export const BUTTON_COPY: Button = {
12 | icon: IconClone,
13 | title: '复制',
14 | handle(field, api){
15 | if(field.allowClone === false) return
16 |
17 | const scope = field.parent
18 | const newField = field.clone()
19 | scope.insert(scope.indexOf(field) + 1, newField)
20 | api.updateSchema()
21 | api.chooseField(newField)
22 | }
23 | }
24 |
25 | export const BUTTON_REMOVE: Button = {
26 | icon: IconRemove,
27 | title: '删除',
28 | handle(field, api, instance){
29 | if(field.allowRemove === false) return
30 |
31 | const name = genEventName(EVENTS.REMOVE)
32 | const listener = instance.vnode?.props?.[name]
33 | const useDefault = function(){
34 | const scope = field.parent
35 | scope.remove(field)
36 | api.chooseField(null)
37 | api.updateSchema()
38 |
39 | nextTick(() => {
40 | // TODO: 是否从`scope.remove`方法中触发
41 | const hook = field.conf?.onRemoved
42 | isFunction(hook) && hook(field, scope, instance)
43 | })
44 | }
45 |
46 | isFunction(listener) ? instance.emit(EVENTS.REMOVE, { field, useDefault }): useDefault()
47 | }
48 | }
49 |
50 | export const BUTTON_PICK_UP: Button = {
51 | icon: IconPickUp,
52 | title: '选中上一级',
53 | handle(field, api, instance, event){
54 | const target = event.target as HTMLElement
55 | const draggableEl = target.closest(SELECTOR.DRAGGABLE)
56 | if(draggableEl == null) return
57 |
58 | const parentEl = draggableEl.parentElement?.closest(SELECTOR.DRAGGABLE)
59 | if(parentEl == null) return
60 |
61 | const parent = getField(parentEl)
62 | if(parent == null) return
63 |
64 | api.chooseField(parent)
65 | }
66 | }
--------------------------------------------------------------------------------
/packages/element-plus/logic/date.tsx:
--------------------------------------------------------------------------------
1 | import classes from './index.module.scss'
2 |
3 | import { Field, FormFieldLogic, isEmpty } from '@dongls/xform'
4 | import { Operators as O } from '@common/operator'
5 | import { createWarnTip } from './common'
6 |
7 | interface DateLogic extends FormFieldLogic {
8 | operator?: string
9 | }
10 |
11 | export const DATE_VALUE_COMPARE = Field.createFieldLogic({
12 | type: 'date_value_compare',
13 | title: '值',
14 | render(logic: DateLogic, field){
15 | const targetField = field.previousField(logic.field)
16 | if(targetField == null) return createWarnTip(logic, field)
17 |
18 | const options = [
19 | O.OPERATOR_EQ,
20 | O.OPERATOR_NE,
21 | O.OPERATOR_LT,
22 | O.OPERATOR_LTE,
23 | O.OPERATOR_GT,
24 | O.OPERATOR_GTE,
25 | O.OPERATOR_EMPTY
26 | ].map(o => )
27 |
28 | const operator = {options}
29 | const value = (
30 | logic.operator === O.OPERATOR_EMPTY.type
31 | ? null
32 | :
33 | )
34 |
35 | return (
36 |
37 | 如果
38 | {targetField ? targetField.title : 'N/A'}
39 | 的值
40 | {operator}
41 | {value}
42 |
43 | )
44 | },
45 | test(logic: DateLogic, field){
46 | const operator = O.get(logic.operator)
47 | if(operator == null) return true
48 |
49 | const target = field.previousField(logic.field)
50 | if(target == null) return true
51 |
52 | return operator.test(target.value, logic.value)
53 | },
54 | validator(logic: DateLogic){
55 | if(isEmpty(logic.value)) return '请补全目标值'
56 | return true
57 | },
58 | onCreated(logic: DateLogic){
59 | if(logic.operator == null) {
60 | logic.operator = O.OPERATOR_EQ.type
61 | }
62 | },
63 | })
--------------------------------------------------------------------------------
/document/docs/components/XFormBuilder.md:
--------------------------------------------------------------------------------
1 | # XFormBuilder 表单生成器
2 | 该组件主要用于根据`XFormDesigner`生成的表单配置,渲染出对应的表单。
3 |
4 | ## 基本用法
5 | ```html
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
35 | ```
36 |
37 | ## Props
38 | ### *schema*
39 | - **类型**:`XFormSchema`
40 | - **说明**:表单的配置,数据来自表单设计器,**必须提供**。
41 | ### *value*
42 | - **类型**:`object`
43 | - **说明**:表单的值,**必须提供**。
44 | ### tag
45 | - **类型**:`string`,
46 | - **默认值**:`form`,
47 | - **说明**:如果您想让`XFormBuilder`渲染为其他标签,比如将该组件作为放入一个已存在`form`表单中,就需要修改`tag`的值。
48 |
49 | ## Slots
50 | ### header
51 | 用于定义组件顶部内容,例如您可以为表单添加一个标题。
52 | ### footer
53 | 用于定义组件底部内容,例如您可以将表单提交按钮放在此处。
54 | ### name_[[name]]
55 | 根据字段的`name`属性定制某一个字段的表单组件,例如:
56 | ```html
57 |
58 |
59 |
60 |
61 |
62 | ```
63 | ### type_[[type]]
64 | 根据字段的`type`属性定制某一类字段类型的表单组件,例如:
65 | ```html
66 |
67 |
68 |
69 |
70 |
71 | ```
72 | ## Events
73 | ### submit
74 | 当`tag`属性为`form`时,如果表单内存在提交按钮,会触发该原生事件。可以在此提交表单或者做其他需要的事情。
75 |
76 | ### update:schema
77 | 表单数据表更时触发。
78 |
79 | ### change
80 | 表单数据更新时触发。
--------------------------------------------------------------------------------
/packages/bootstrap/logic/date.tsx:
--------------------------------------------------------------------------------
1 | import classes from './index.module.scss'
2 |
3 | import { Field, FormFieldLogic, isEmpty } from '@dongls/xform'
4 | import { Operators as O } from '@common/operator'
5 | import { createWarnTip } from './common'
6 |
7 | interface DateLogic extends FormFieldLogic {
8 | operator?: string
9 | }
10 |
11 | export const DATE_VALUE_COMPARE = Field.createFieldLogic({
12 | type: 'date_value_compare',
13 | title: '值',
14 | render(logic: DateLogic, field){
15 | const targetField = field.previousField(logic.field)
16 | if(targetField == null) return createWarnTip(logic, field)
17 |
18 | const options = [
19 | O.OPERATOR_EQ,
20 | O.OPERATOR_NE,
21 | O.OPERATOR_LT,
22 | O.OPERATOR_LTE,
23 | O.OPERATOR_GT,
24 | O.OPERATOR_GTE,
25 | O.OPERATOR_EMPTY
26 | ].map(o => )
27 |
28 | const operator =
29 | const value = (
30 | logic.operator === O.OPERATOR_EMPTY.type
31 | ? null
32 | :
33 | )
34 |
35 | return (
36 |
37 | 如果
38 | {targetField ? targetField.title : 'N/A'}
39 | 的值
40 | {operator}
41 | {value}
42 |
43 | )
44 | },
45 | test(logic: DateLogic, field){
46 | const operator = O.get(logic.operator)
47 | if(operator == null) return true
48 |
49 | const target = field.previousField(logic.field)
50 | if(target == null) return true
51 |
52 | return operator.test(target.value, logic.value)
53 | },
54 | validator(logic: DateLogic){
55 | if(isEmpty(logic.value)) return '请补全目标值'
56 | return true
57 | },
58 | onCreated(logic: DateLogic){
59 | if(logic.operator == null) {
60 | logic.operator = O.OPERATOR_EQ.type
61 | }
62 | },
63 | })
--------------------------------------------------------------------------------
/packages/element-plus/logic/number.tsx:
--------------------------------------------------------------------------------
1 | import classes from './index.module.scss'
2 |
3 | import { Field, FormFieldLogic, isEmpty } from '@dongls/xform'
4 | import { Operators as O } from '@common/operator'
5 | import { createWarnTip } from './common'
6 |
7 | interface NumberLogic extends FormFieldLogic {
8 | operator?: string
9 | }
10 |
11 | export const NUMBER_VALUE_COMPARE = Field.createFieldLogic({
12 | type: 'number_value_compare',
13 | title: '值',
14 | render(logic: NumberLogic, field){
15 | const targetField = field.previous().find(f => f.name == logic.field)
16 | if(targetField == null) return createWarnTip(logic, field)
17 |
18 | const options = [
19 | O.OPERATOR_EQ,
20 | O.OPERATOR_NE,
21 | O.OPERATOR_LT,
22 | O.OPERATOR_LTE,
23 | O.OPERATOR_GT,
24 | O.OPERATOR_GTE
25 | ].map(o => )
26 |
27 | const operator = {options}
28 | const value = (
29 | logic.operator === O.OPERATOR_EMPTY.type
30 | ? null
31 | :
32 | )
33 |
34 | return (
35 |
36 | 如果
37 | {targetField ? targetField.title : 'N/A'}
38 | 的值
39 | {operator}
40 | {value}
41 |
42 | )
43 | },
44 | test(logic: NumberLogic, field){
45 | const operator = O.get(logic.operator)
46 | if(operator == null) return true
47 |
48 | const target = field.previousField(logic.field)
49 | if(target == null) return true
50 |
51 | const value = target.value
52 | if(typeof value != 'number') return false
53 |
54 | return operator.test(value, logic.value)
55 | },
56 | validator(logic: NumberLogic){
57 | if(isEmpty(logic.value)) return '请补全目标值'
58 | return true
59 | },
60 | onCreated(logic: NumberLogic){
61 | if(logic.operator == null) {
62 | logic.operator = O.OPERATOR_EQ.type
63 | }
64 | },
65 | })
--------------------------------------------------------------------------------
/packages/bootstrap/util.ts:
--------------------------------------------------------------------------------
1 | import { useConstant } from '@dongls/xform'
2 |
3 | export {
4 | useValue,
5 | useField,
6 | useFieldProp,
7 | useDefaultValueApi,
8 | useOptions
9 | } from '@common/util'
10 |
11 | const { EVENTS } = useConstant()
12 |
13 | // retrieve raw value set via :value bindings
14 | function getValue(el: HTMLOptionElement | HTMLInputElement) {
15 | return '_value' in el ? (el as any)._value : el.value
16 | }
17 |
18 | // retrieve raw value for true-value and false-value set via :true-value or :false-value bindings
19 | function getCheckboxValue(el: HTMLInputElement & { _trueValue?: any; _falseValue?: any }) {
20 | const checked = el.checked
21 | const key = checked ? '_trueValue' : '_falseValue'
22 | return key in el ? el[key] : checked
23 | }
24 |
25 | function parseCheckedValue(element: HTMLInputElement){
26 | const value = getValue(element)
27 | return value == null ? element.checked : element.value
28 | }
29 |
30 | export function parseSelectValue(target: HTMLSelectElement){
31 | const selectedIndex = target.selectedIndex
32 | return selectedIndex < 0 ? null : getValue(target.options[selectedIndex])
33 | }
34 |
35 | function parseValue(target: any, behavior = 'build'){
36 | const value: any = target.value
37 | const type: string = target.type
38 |
39 | if(type == 'number') {
40 | const n = parseFloat(value)
41 | return isNaN(n) ? value : n
42 | }
43 |
44 | if(type == 'select') return parseSelectValue(target as HTMLSelectElement)
45 |
46 | if(type == 'checkbox'){
47 | const selector = `input[type="checkbox"][name="${target.name}"]`
48 | const elements = Array.from(document.querySelectorAll(selector)) as HTMLInputElement[]
49 | if(behavior === 'setting' && elements.length == 1) return getCheckboxValue(elements[0])
50 |
51 | return elements.filter(e => e.checked).map(parseCheckedValue)
52 | }
53 |
54 | return target.value
55 | }
56 |
57 | export function updateField(emit: Function, event: Event, prop: string, scope?: string){
58 | const target = event.target
59 | const value = parseValue(target, 'setting')
60 |
61 | emit(EVENTS.UPDATE_FIELD, { prop, value, scope })
62 | }
63 |
--------------------------------------------------------------------------------
/packages/bootstrap/logic/number.tsx:
--------------------------------------------------------------------------------
1 | import classes from './index.module.scss'
2 |
3 | import { Field, FormFieldLogic, isEmpty } from '@dongls/xform'
4 | import { Operators as O } from '@common/operator'
5 | import { createWarnTip } from './common'
6 |
7 | interface NumberLogic extends FormFieldLogic {
8 | operator?: string
9 | }
10 |
11 | export const NUMBER_VALUE_COMPARE = Field.createFieldLogic({
12 | type: 'number_value_compare',
13 | title: '值',
14 | render(logic: NumberLogic, field){
15 | const targetField = field.previous().find(f => f.name == logic.field)
16 | if(targetField == null) return createWarnTip(logic, field)
17 |
18 | const options = [
19 | O.OPERATOR_EQ,
20 | O.OPERATOR_NE,
21 | O.OPERATOR_LT,
22 | O.OPERATOR_LTE,
23 | O.OPERATOR_GT,
24 | O.OPERATOR_GTE
25 | ].map(o => )
26 |
27 | const operator =
28 | const value = (
29 | logic.operator === O.OPERATOR_EMPTY.type
30 | ? null
31 | :
32 | )
33 |
34 | return (
35 |
36 | 如果
37 | {targetField ? targetField.title : 'N/A'}
38 | 的值
39 | {operator}
40 | {value}
41 |
42 | )
43 | },
44 | test(logic: NumberLogic, field){
45 | const operator = O.get(logic.operator)
46 | if(operator == null) return true
47 |
48 | const target = field.previousField(logic.field)
49 | if(target == null) return true
50 |
51 | const value = target.value
52 | if(typeof value != 'number') return false
53 |
54 | return operator.test(value, logic.value)
55 | },
56 | validator(logic: NumberLogic){
57 | if(isEmpty(logic.value)) return '请补全目标值'
58 | return true
59 | },
60 | onCreated(logic: NumberLogic){
61 | if(logic.operator == null) {
62 | logic.operator = O.OPERATOR_EQ.type
63 | }
64 | },
65 | })
--------------------------------------------------------------------------------
/packages/core/api/Logic/builtin.tsx:
--------------------------------------------------------------------------------
1 | import { Field } from '../../model/Field'
2 | import { registerLogic, test } from './logic'
3 |
4 | export const BUILTIN_LOGIC_AND = Field.createFieldLogic({
5 | type: 'and',
6 | title: '逻辑与',
7 | composed: true,
8 | test(logic, field) {
9 | const conditions = logic.conditions
10 | if(!Array.isArray(conditions)) return false
11 |
12 | for(const c of conditions) {
13 | if(!test(c, field)) return false
14 | }
15 |
16 | return true
17 | },
18 | render(){
19 | return 如果以下所有条件均被满足:
20 | },
21 | validator(logic){
22 | if(!Array.isArray(logic.conditions) || logic.conditions.length <= 0){
23 | return '至少添加一条子逻辑'
24 | }
25 | }
26 | })
27 |
28 | export const BUILTIN_LOGIC_OR = Field.createFieldLogic({
29 | type: 'or',
30 | title: '逻辑或',
31 | composed: true,
32 | test(logic, field) {
33 | const conditions = logic.conditions
34 | if(!Array.isArray(conditions)) return false
35 |
36 | for(const c of conditions) {
37 | if(test(c, field)) return true
38 | }
39 |
40 | return false
41 | },
42 | render(){
43 | return 如果以下任意条件被满足:
44 | },
45 | validator(logic){
46 | if(!Array.isArray(logic.conditions) || logic.conditions.length <= 0){
47 | return '至少添加一条子逻辑'
48 | }
49 | }
50 | })
51 |
52 | export const BUILTIN_LOGIC_NOT = Field.createFieldLogic({
53 | type: 'not',
54 | title: '逻辑非',
55 | composed: true,
56 | test(logic, field) {
57 | const conditions = logic.conditions
58 | if(!Array.isArray(conditions)) return false
59 |
60 | for(const c of conditions) {
61 | if(test(c, field)) return false
62 | }
63 |
64 | return true
65 | },
66 | render(){
67 | return 如果以下所有条件均不被满足:
68 | },
69 | validator(logic){
70 | if(!Array.isArray(logic.conditions) || logic.conditions.length <= 0){
71 | return '至少添加一条子逻辑'
72 | }
73 | }
74 | })
75 |
76 | export function useBuiltIn(){
77 | registerLogic(BUILTIN_LOGIC_AND)
78 | registerLogic(BUILTIN_LOGIC_OR)
79 | registerLogic(BUILTIN_LOGIC_NOT)
80 | }
--------------------------------------------------------------------------------
/packages/bootstrap/fields/date/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
26 |
27 |
28 |
29 |
67 |
68 |
82 |
--------------------------------------------------------------------------------
/packages/core/util/component.ts:
--------------------------------------------------------------------------------
1 | import { ComponentInternalInstance, ComponentOptions, nextTick, VNode } from 'vue'
2 | import { RawProps, FormField, EnumComponent, FieldComponent, SELECTOR } from '../model'
3 | import { isObject, isPlainObject, isString } from './lang'
4 |
5 | /** 获取字段配置的组件 */
6 | export function getFieldComponent(field: FormField, target: EnumComponent, mode?: string){
7 | const r = field.conf?.[target]
8 | return (r instanceof FieldComponent ? r.get(field, mode) : r) as ComponentOptions | VNode
9 | }
10 |
11 | /** 获取ref对应的dom或组件 */
12 | export function getRef(refs: Record, key: string): T{
13 | return refs[key] as T
14 | }
15 |
16 | /** 获取ref对应的HTMLElement */
17 | export function getHtmlElement(refs: Record, key: string){
18 | return getRef(refs, key)
19 | }
20 |
21 | export function fillComponentProps(component: unknown, all: RawProps, base: RawProps = {}){
22 | const { props, emits } = component as { props?: object, emits?: string[] }
23 |
24 | let defs: string[] = []
25 | if(null != props){
26 | defs = defs.concat(Object.keys(props))
27 | }
28 |
29 | if(Array.isArray(emits)){
30 | defs = defs.concat(emits.map(genEventName))
31 | }
32 |
33 | return defs.reduce((acc, key) => {
34 | const v = all[key]
35 | if(v != null) acc[key] = v
36 | return acc
37 | }, base)
38 | }
39 |
40 | export function genEventName(name: string){
41 | return 'on' + name[0].toUpperCase() + name.slice(1)
42 | }
43 |
44 | function _normalizeClass(value: unknown){
45 | if(isObject(value)) return value
46 |
47 | if(isString(value)){
48 | value = value.split(' ').filter(v => v)
49 | }
50 |
51 | if(Array.isArray(value)){
52 | return value.reduce((acc, v) => {
53 | acc[v] = true
54 | return acc
55 | }, {} as any)
56 | }
57 |
58 | return {}
59 | }
60 |
61 | export function normalizeClass(value: unknown, o?: unknown){
62 | const klass = _normalizeClass(value)
63 | return isPlainObject(o) ? Object.assign(klass, o) : klass
64 | }
65 |
66 | export function showSelectedField(instance: ComponentInternalInstance){
67 | return nextTick(() => {
68 | const target = getHtmlElement(instance.refs, 'list').querySelector(SELECTOR.IS_SELECTED)
69 | if(target) target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
70 | })
71 | }
--------------------------------------------------------------------------------
/packages/element-plus/fields/date/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
25 |
26 |
27 |
68 |
69 |
86 |
--------------------------------------------------------------------------------
/document/assets/style/document/article.css:
--------------------------------------------------------------------------------
1 | .article h1{
2 | padding-top: 0;
3 | margin-bottom: 40px;
4 | }
5 |
6 | .article > h3{
7 | font-style: italic;
8 | }
9 |
10 | .article-toc{
11 | position: fixed;
12 | width: 180px;
13 | margin: 0;
14 | padding-left: 34px;
15 | }
16 |
17 | .article-toc > li{
18 | height: 20px;
19 | line-height: 20px;
20 | }
21 |
22 | .article-toc > li.active{
23 | border-left-color: var(--doc-color-primary);
24 | }
25 |
26 | .article-toc > li.active a{
27 | color: var(--doc-link-color) !important;
28 | }
29 |
30 | .article-toc > li + li{
31 | margin-top: 5px;
32 | }
33 |
34 | .article-toc > li > a:link,
35 | .article-toc > li > a:visited{
36 | color: #333;
37 | }
38 |
39 | .article-toc > li > a:hover{
40 | color: var(--doc-link-color);
41 | }
42 |
43 | .article-anchor{
44 | float: left;
45 | margin-left: -15px !important;
46 | }
47 |
48 | .article-anchor:hover{
49 | background-color: transparent;
50 | border-bottom-color: transparent;
51 | }
52 |
53 | .article-sticky-heading{
54 | position: sticky;
55 | top: 45px;
56 | background-color: #fff;
57 | z-index: 9;
58 | }
59 |
60 | .event-table th:nth-child(1){
61 | width: 160px;
62 | }
63 |
64 | .event-table th:nth-child(2){
65 | width: 180px;
66 | }
67 |
68 | /* ================================= [markdown-it-container] ================================= */
69 | .md-container{
70 | padding: 15px;
71 | margin-bottom: 10px;
72 | border-radius: 1px;
73 | }
74 |
75 | .md-container-title{
76 | font-size: 16px;
77 | font-weight: 700;
78 | margin: 0 0 10px 0;
79 | line-height: 20px;
80 | }
81 |
82 | .md-container > *:last-child{
83 | margin-bottom: 0 !important;
84 | }
85 |
86 | .md-container-tip{
87 | background-color: #e6f7ff;
88 | }
89 |
90 | .md-container-warning{
91 | background-color: rgba(255,229,100,.3);
92 | border-color: #e7c000;
93 | color: #6b5900;
94 | }
95 |
96 | /* .doc-container-warning .doc-container-title{
97 | color: #b29400;
98 | } */
99 |
100 | .md-container-danger{
101 | background-color: #f8d7da;
102 | color: #721c24;
103 | }
104 |
105 | /* .doc-container-danger .doc-container-title{
106 | color: #721c24;
107 | } */
108 | /* ================================= [markdown-it-container] ================================= */
109 |
--------------------------------------------------------------------------------
/packages/bootstrap/FieldLogic.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
78 |
79 |
--------------------------------------------------------------------------------
/document/assets/style/document/highlight.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Atom One Light by Daniel Gamage
4 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax
5 |
6 | base: #fafafa
7 | mono-1: #383a42
8 | mono-2: #686b77
9 | mono-3: #a0a1a7
10 | hue-1: #0184bb
11 | hue-2: #4078f2
12 | hue-3: #a626a4
13 | hue-4: #50a14f
14 | hue-5: #e45649
15 | hue-5-2: #c91243
16 | hue-6: #986801
17 | hue-6-2: #c18401
18 |
19 | */
20 |
21 | .hljs {
22 | position: relative;
23 | display: block;
24 |
25 | color: #383a42;
26 | background: #f8f9fa;
27 | border-radius: 1px;
28 | line-height: 18px;
29 | }
30 |
31 | .hljs[language]::after{
32 | content: attr(language);
33 | position: absolute;
34 | top: 5px;
35 | right: 8px;
36 | color: #ccc;
37 | font-size: 12px;
38 | line-height: 1;;
39 | font-weight: 600;
40 | font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
41 | user-select: none;
42 | }
43 |
44 | .hljs code{
45 | overflow-x: auto;
46 | padding: 15px;
47 | display: block;
48 | font-size: 14px;
49 | font-family: Cascadia Code, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
50 | }
51 |
52 | .hljs-comment,
53 | .hljs-quote {
54 | color: #a0a1a7;
55 | font-style: italic;
56 | }
57 |
58 | .hljs-doctag,
59 | .hljs-keyword,
60 | .hljs-formula {
61 | color: #a626a4;
62 | }
63 |
64 | .hljs-section,
65 | .hljs-name,
66 | .hljs-selector-tag,
67 | .hljs-deletion,
68 | .hljs-subst {
69 | color: #e45649;
70 | }
71 |
72 | .hljs-literal {
73 | color: #0184bb;
74 | }
75 |
76 | .hljs-string,
77 | .hljs-regexp,
78 | .hljs-addition,
79 | .hljs-attribute,
80 | .hljs-meta-string {
81 | color: #50a14f;
82 | }
83 |
84 | .hljs-built_in,
85 | .hljs-class .hljs-title {
86 | color: #c18401;
87 | }
88 |
89 | .hljs-attr,
90 | .hljs-variable,
91 | .hljs-template-variable,
92 | .hljs-type,
93 | .hljs-selector-class,
94 | .hljs-selector-attr,
95 | .hljs-selector-pseudo,
96 | .hljs-number {
97 | color: #986801;
98 | }
99 |
100 | .hljs-symbol,
101 | .hljs-bullet,
102 | .hljs-link,
103 | .hljs-meta,
104 | .hljs-selector-id,
105 | .hljs-title {
106 | color: #4078f2;
107 | }
108 |
109 | .hljs-emphasis {
110 | font-style: italic;
111 | }
112 |
113 | .hljs-strong {
114 | font-weight: bold;
115 | }
116 |
117 | .hljs-link {
118 | text-decoration: underline;
119 | }
120 |
--------------------------------------------------------------------------------
/packages/element-plus/fields/divider/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
25 |
26 |
27 |
28 |
58 |
59 |
--------------------------------------------------------------------------------
/packages/core/util/dom.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FormField,
3 | PROPS,
4 | FormScope
5 | } from '../model'
6 |
7 | import { isString } from './lang'
8 |
9 | /**
10 | * 查找坐标点下第一个符合的元素
11 | * @param x - 坐标点的水平坐标值
12 | * @param y - 坐标点的垂向坐标值
13 | * @param selector - 目标选择器
14 | */
15 | export function findElementFromPoint(x: number, y: number, selector: string | string[], scope?: Element){
16 | const elementsFromPoint = document.elementsFromPoint || document.msElementsFromPoint
17 | if(typeof elementsFromPoint !== 'function') return null
18 |
19 | const elements: Element[] = elementsFromPoint.call(document, x, y)
20 | for(const element of elements){
21 | if(scope != null && !scope.contains(element)) continue
22 | if(isString(selector) && element.matches(selector)) return element
23 | if(Array.isArray(selector) && selector.some(selector => element.matches(selector))) return element
24 | }
25 |
26 | return null
27 | }
28 |
29 | /** 查询坐标点下符合条件的元素 */
30 | export function findElementsFromPoint(x: number, y: number, selector: string | string[], scope?: Element){
31 | const elementsFromPoint = document.elementsFromPoint || document.msElementsFromPoint
32 | if(typeof elementsFromPoint !== 'function') return null
33 |
34 | const elements: Element[] = elementsFromPoint.call(document, x, y)
35 | return elements.filter(element => {
36 | if(scope != null && !scope.contains(element)) return false
37 | if(isString(selector)) return element.matches(selector)
38 | if(Array.isArray(selector)) return selector.some(selector => element.matches(selector))
39 | return false
40 | })
41 | }
42 |
43 | /**
44 | * 统一浏览器之间wheel事件的差异
45 | * @see https://github.com/basilfx/normalize-wheel
46 | * @param event - 事件对象
47 | */
48 | export function normalizeWheel(event: WheelEvent){
49 | const { deltaX, deltaY } = event
50 | const unit = event.deltaMode == 0 ? 1 : event.deltaMode == 1 ? 40 : 800
51 |
52 | return { pixelX: deltaX * unit, pixelY: deltaY * unit }
53 | }
54 |
55 | /** 获取dom元素上的属性 */
56 | export function getProperty(element: Element, key: string){
57 | return (element as any)[key] as T
58 | }
59 |
60 | /** 获取dom元素上绑定的XField字段值 */
61 | export function getField(element: Element){
62 | return getProperty(element, PROPS.FIELD)
63 | }
64 |
65 | /** 获取dom元素上绑定的scope */
66 | export function getScope(element: Element){
67 | return getProperty(element, PROPS.SCOPE) ?? getProperty(element, PROPS.FIELD)
68 | }
--------------------------------------------------------------------------------
/document/docs/components/XFormDesigner.md:
--------------------------------------------------------------------------------
1 | # XFormDesigner 表单设计器
2 | 该组件提供了一个图形化的表单设计器,用户可以通过拖拽的方式快速配置出表单。
3 | ## 基本用法
4 |
5 | ```html
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
26 | ```
27 |
28 | ## Props
29 | ### schema
30 | - **类型**:`XFormSchema`
31 | - **说明**:设计器生成的表单配置,**必须提供**。 你可以使用`createSchema`方法创建它或者通过其他方式自行创建。需要注意的是,**`fields`的类型必须是`XField[]`**。
32 | ```javascript
33 | import {createSchema} from '@dongls/xform';
34 | const schema = createSchema({/* 传入你的数据 */});
35 |
36 | // 如果是通过其他方式创建的对象,需要将fields做类型转换
37 | schema.fields = schema.fields.map(f => f instanceof XField ? f : new XField(f))
38 | ```
39 |
40 | ### mode
41 | - **类型**:`string`
42 | - **默认值**:`null`
43 | - **说明**:如果您希望组件**在不同的场景下展示不同的字段类型**,您可以通过配置`modes`来定义多种模式来满足您的需求。 如果该属性的值为`null`,那么将显示所有注册的字段类型。例如:
44 | ```javascript
45 | app.use(XForm, {
46 | config: {
47 | modes: {
48 | example: [ // 字段类型分组
49 | { title: '分组1', types: ['type1', 'type2'] },
50 | { title: '分组2', types: ['type3', 'type4'] }
51 | ],
52 | simple: ['type1', 'type2', 'type4']
53 | }
54 | }
55 | });
56 | ```
57 | 然后就可以在组件中使用`mode`属性来展示不同的字段类型。
58 | ```html
59 |
60 |
61 | ```
62 |
63 | ## Slots
64 |
65 | ### tool
66 | 用于定义组件顶部工具条。
67 | ### setting
68 | 用于定制表单设置。需要注意的是,如果`preset.slots.setting`和该插槽都不存在的话,组件将不会显示表单设置。
69 | ### setting_name_[[target]]
70 | 根据字段的`name`属性定制某一个字段的设置组件。
71 | ```html
72 |
73 |
74 | 定制name值为demo的字段的设置组件
75 |
76 |
77 | ```
78 | ### setting_type_[[target]]
79 | 根据字段的`type`属性定制某一类型字段的设置组件。
80 | ```html
81 |
82 |
83 | 定制所有字段类型为text的字段的设置组件
84 |
85 |
86 | ```
87 |
88 | ## Events
89 | ### update:schema
90 | 表单数据表更时触发。
--------------------------------------------------------------------------------
/packages/common/svg/tabs.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/core/component/FormItem/component.css:
--------------------------------------------------------------------------------
1 | .xform-item{
2 | box-sizing: border-box;
3 | display: flex;
4 | flex-flow: row nowrap;
5 |
6 | font-size: var(--xform-font-size);
7 | color: var(--xform-text-color);
8 | }
9 |
10 | .xform-builder .xform-item,
11 | .xform-viewer .xform-item{
12 | margin-bottom: 24px;
13 | }
14 |
15 | .xform-item.xform-is-required .xform-item-label{
16 | font-weight: 700;
17 | }
18 |
19 | .xform-item.xform-is-required .xform-item-label > span.xform-item-title:before{
20 | content: "*";
21 | position: absolute;
22 | top: 0;
23 | left: -10px;
24 | color: var(--xform-color-danger);
25 | font-size: 18px;
26 | line-height: 1;
27 | font-weight: 700;
28 | }
29 |
30 | .xform-is-left .xform-item-label{
31 | text-align: left;
32 | }
33 |
34 | .xform-is-right .xform-item-label{
35 | text-align: right;
36 | }
37 |
38 | .xform-is-top{
39 | flex-direction: column;
40 | }
41 |
42 | .xform-is-top .xform-item-label{
43 | width: auto !important;
44 | padding-left: 0;
45 | padding-top: 0;
46 | padding-bottom: 4px;
47 | }
48 |
49 | .xform-is-top.xform-is-required .xform-item-label{
50 | padding-left: 10px ;
51 | }
52 |
53 | .xform-is-top .xform-item-content{
54 | width: 100%;
55 | }
56 |
57 | .xform-item-label{
58 | box-sizing: border-box;
59 | width: var(--xform-label-width);
60 | padding: 5px 10px 0 10px;
61 | line-height: 20px;
62 | word-break: break-all;
63 | margin: 0;
64 | }
65 |
66 | .xform-item-label > span{
67 | position: relative;
68 | }
69 |
70 | .xform-item-content{
71 | box-sizing: border-box;
72 | width: 0;
73 | flex: 1;
74 | line-height: 20px;
75 | position: relative;
76 | }
77 |
78 | i.xform-item-help-icon{
79 | font-size: 14px;
80 | margin-left: 2px;
81 | color: #909399;
82 | cursor: help;
83 | }
84 |
85 | pre.xform-item-help-content{
86 | margin: 0;
87 | font-family: inherit;
88 | line-height: 1.25;
89 | white-space: pre-line;
90 | max-width: 480px;
91 | max-height: 320px;
92 | overflow: auto;
93 | }
94 |
95 | .xform-is-error .xform-item-control{
96 | border-color: var(--xform-color-danger);
97 | }
98 |
99 | .xform-is-error .xform-item-message{
100 | color: var(--xform-color-danger);
101 | }
102 |
103 | .xform-is-validating,
104 | .xform-item-message{
105 | position: absolute;
106 | color: #606266;
107 | font-size: 12px;
108 | line-height: 20px;
109 | margin: 0;
110 | white-space: pre-line;
111 | }
112 |
113 |
--------------------------------------------------------------------------------