mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
feat: 添加 InputFormula 公式编辑器 (#3227)
* feat: 添加 inputFormula * feat: 添加 inputFormula * 先简单处理一下代码高亮 * sdk 编译时 codemirror 单独打包
This commit is contained in:
parent
389e2ab1c4
commit
8301e67f83
61
docs/zh-CN/components/form/input-formula.md
Normal file
61
docs/zh-CN/components/form/input-formula.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: InputFormula 公式编辑器
|
||||||
|
description:
|
||||||
|
type: 0
|
||||||
|
group: null
|
||||||
|
menuName: InputFormula
|
||||||
|
icon:
|
||||||
|
order: 21
|
||||||
|
---
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
用来输入公式。还是 beta 版本,整体待优化。
|
||||||
|
|
||||||
|
```schema: scope="formitem"
|
||||||
|
{
|
||||||
|
"type": "input-formula",
|
||||||
|
"name": "formula",
|
||||||
|
"label": "公式",
|
||||||
|
"variableMode": "tabs",
|
||||||
|
"evalMode": false,
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"label": "表单字段",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "ID",
|
||||||
|
"value": "id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "ID2",
|
||||||
|
"value": "id2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "流程字段",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "ID",
|
||||||
|
"value": "id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "ID2",
|
||||||
|
"value": "id2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 属性表
|
||||||
|
|
||||||
|
| 属性名 | 类型 | 默认值 | 说明 |
|
||||||
|
| ------------ | --------------------------------------------------- | ------ | ------------------------------------------------------------------------------ |
|
||||||
|
| header | string | | 弹出来的弹框标题 |
|
||||||
|
| evalMode | Boolean | true | 表达式模式 或者 模板模式,模板模式则需要将表达式写在 `${` 和 `}` 中间。 |
|
||||||
|
| variables | {label: string; value: string; children?: any[];}[] | [] | 可用变量 |
|
||||||
|
| variableMode | string | `list` | 可配置成 `tabs` 或者 `tree` 默认为列表,支持分组。 |
|
||||||
|
| functions | Object[] | | 可以不设置,默认就是 amis-formula 里面定义的函数,如果扩充了新的函数则需要指定 |
|
@ -370,6 +370,14 @@ export const components = [
|
|||||||
makeMarkdownRenderer
|
makeMarkdownRenderer
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'InputFormula 公式编辑器',
|
||||||
|
path: '/zh-CN/components/form/input-formula',
|
||||||
|
getComponent: () =>
|
||||||
|
import('../../docs/zh-CN/components/form/input-formula.md').then(
|
||||||
|
makeMarkdownRenderer
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'DiffEditor 对比编辑器',
|
label: 'DiffEditor 对比编辑器',
|
||||||
path: '/zh-CN/components/form/diff-editor',
|
path: '/zh-CN/components/form/diff-editor',
|
||||||
|
@ -495,6 +495,7 @@ if (fis.project.currentMedia() === 'publish') {
|
|||||||
'!mpegts.js/**',
|
'!mpegts.js/**',
|
||||||
'!hls.js/**',
|
'!hls.js/**',
|
||||||
'!froala-editor/**',
|
'!froala-editor/**',
|
||||||
|
'!codemirror/**',
|
||||||
|
|
||||||
'!tinymce/**',
|
'!tinymce/**',
|
||||||
'!zrender/**',
|
'!zrender/**',
|
||||||
@ -530,6 +531,7 @@ if (fis.project.currentMedia() === 'publish') {
|
|||||||
|
|
||||||
'tinymce.js': ['src/components/Tinymce.tsx', 'tinymce/**'],
|
'tinymce.js': ['src/components/Tinymce.tsx', 'tinymce/**'],
|
||||||
|
|
||||||
|
'codemirror.js': ['codemirror/**'],
|
||||||
'papaparse.js': ['papaparse/**'],
|
'papaparse.js': ['papaparse/**'],
|
||||||
|
|
||||||
'exceljs.js': ['exceljs/**'],
|
'exceljs.js': ['exceljs/**'],
|
||||||
@ -562,6 +564,7 @@ if (fis.project.currentMedia() === 'publish') {
|
|||||||
'rest.js': [
|
'rest.js': [
|
||||||
'*.js',
|
'*.js',
|
||||||
'!monaco-editor/**',
|
'!monaco-editor/**',
|
||||||
|
'!codemirror/**',
|
||||||
'!mpegts.js/**',
|
'!mpegts.js/**',
|
||||||
'!hls.js/**',
|
'!hls.js/**',
|
||||||
'!froala-editor/**',
|
'!froala-editor/**',
|
||||||
@ -770,6 +773,7 @@ if (fis.project.currentMedia() === 'publish') {
|
|||||||
'/examples/mod.js',
|
'/examples/mod.js',
|
||||||
'node_modules/**.js',
|
'node_modules/**.js',
|
||||||
'!monaco-editor/**',
|
'!monaco-editor/**',
|
||||||
|
'!codemirror/**',
|
||||||
'!mpegts.js/**',
|
'!mpegts.js/**',
|
||||||
'!hls.js/**',
|
'!hls.js/**',
|
||||||
'!froala-editor/**',
|
'!froala-editor/**',
|
||||||
@ -808,6 +812,8 @@ if (fis.project.currentMedia() === 'publish') {
|
|||||||
|
|
||||||
'pkg/tinymce.js': ['src/components/Tinymce.tsx', 'tinymce/**'],
|
'pkg/tinymce.js': ['src/components/Tinymce.tsx', 'tinymce/**'],
|
||||||
|
|
||||||
|
'pkg/codemirror.js': ['codemirror/**'],
|
||||||
|
|
||||||
'pkg/papaparse.js': ['papaparse/**'],
|
'pkg/papaparse.js': ['papaparse/**'],
|
||||||
|
|
||||||
'pkg/exceljs.js': ['exceljs/**'],
|
'pkg/exceljs.js': ['exceljs/**'],
|
||||||
|
@ -88,9 +88,11 @@
|
|||||||
"tinymce": "^5.10.2",
|
"tinymce": "^5.10.2",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"uncontrollable": "7.2.1",
|
"uncontrollable": "7.2.1",
|
||||||
"video-react": "0.14.1"
|
"video-react": "0.14.1",
|
||||||
|
"codemirror": "^5.63.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/codemirror": "^5.60.3",
|
||||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||||
"@testing-library/react": "^12.0.0",
|
"@testing-library/react": "^12.0.0",
|
||||||
"@types/async": "^2.0.45",
|
"@types/async": "^2.0.45",
|
||||||
|
122
scss/components/_formula.scss
Normal file
122
scss/components/_formula.scss
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
.#{$ns}FormulaEditor {
|
||||||
|
overflow: visible;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
width: 100%;
|
||||||
|
height: px2rem(40px);
|
||||||
|
line-height: px2rem(40px);
|
||||||
|
padding-left: px2rem(10px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f3f8fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-editor {
|
||||||
|
min-height: px2rem(238px);
|
||||||
|
max-height: px2rem(320px);
|
||||||
|
height: auto;
|
||||||
|
border: var(--Form-input-borderWidth) solid var(--Form-input-borderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-error &-editor {
|
||||||
|
border-color: var(--Form-input-onError-borderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-focused &-editor {
|
||||||
|
border-color: var(--Form-input-onFocused-borderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-height: px2rem(350px);
|
||||||
|
margin: 0 -5px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-field,
|
||||||
|
.cm-func {
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0 1px;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.cm-field {
|
||||||
|
background: #007bff;
|
||||||
|
}
|
||||||
|
.cm-func {
|
||||||
|
background: #17a2b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$ns}FormulaFuncList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > &-searchBox {
|
||||||
|
display: flex;
|
||||||
|
width: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: px2rem(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-columns {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
> div:first-child {
|
||||||
|
min-width: 200px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-funcItem {
|
||||||
|
padding: 0 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-groupTitle {
|
||||||
|
padding: 5px 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&-groupBody {
|
||||||
|
> div {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-funcDetail {
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$ns}FormulaPicker {
|
||||||
|
&-icon {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
@ -118,5 +118,6 @@
|
|||||||
@import '../components/markdown';
|
@import '../components/markdown';
|
||||||
@import '../components/link';
|
@import '../components/link';
|
||||||
@import '../components/mapping';
|
@import '../components/mapping';
|
||||||
|
@import '../components/formula';
|
||||||
|
|
||||||
@import '../utilities';
|
@import '../utilities';
|
||||||
|
@ -227,6 +227,7 @@ export type SchemaType =
|
|||||||
| 'input-time-range'
|
| 'input-time-range'
|
||||||
| 'input-datetime-range'
|
| 'input-datetime-range'
|
||||||
| 'input-excel'
|
| 'input-excel'
|
||||||
|
| 'input-formula'
|
||||||
| 'diff-editor'
|
| 'diff-editor'
|
||||||
|
|
||||||
// editor 系列
|
// editor 系列
|
||||||
|
99
src/components/CodeMirror.tsx
Normal file
99
src/components/CodeMirror.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import 'codemirror/lib/codemirror.css';
|
||||||
|
import type CodeMirror from 'codemirror';
|
||||||
|
import {autobind} from '../utils/helper';
|
||||||
|
import {resizeSensor} from '../utils/resize-sensor';
|
||||||
|
|
||||||
|
export interface CodeMirrorEditorProps {
|
||||||
|
className?: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onFocus?: (e: any) => void;
|
||||||
|
onBlur?: (e: any) => void;
|
||||||
|
editorFactory?: (
|
||||||
|
dom: HTMLElement,
|
||||||
|
cm: typeof CodeMirror,
|
||||||
|
props?: any
|
||||||
|
) => CodeMirror.Editor;
|
||||||
|
editorDidMount?: (cm: typeof CodeMirror, editor: CodeMirror.Editor) => void;
|
||||||
|
editorWillUnMount?: (
|
||||||
|
cm: typeof CodeMirror,
|
||||||
|
editor: CodeMirror.Editor
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
|
||||||
|
dom = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
editor?: CodeMirror.Editor;
|
||||||
|
toDispose: Array<() => void> = [];
|
||||||
|
unmounted = false;
|
||||||
|
async componentDidMount() {
|
||||||
|
const cm = (await import('codemirror')).default;
|
||||||
|
// @ts-ignore
|
||||||
|
await import('codemirror/mode/javascript/javascript');
|
||||||
|
// @ts-ignore
|
||||||
|
await import('codemirror/mode/htmlmixed/htmlmixed');
|
||||||
|
await import('codemirror/addon/mode/simple');
|
||||||
|
await import('codemirror/addon/mode/multiplex');
|
||||||
|
if (this.unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor =
|
||||||
|
this.props.editorFactory?.(this.dom.current!, cm, this.props) ??
|
||||||
|
cm(this.dom.current!, {
|
||||||
|
value: this.props.value || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.editorDidMount?.(cm, this.editor);
|
||||||
|
this.editor.on('change', this.handleChange);
|
||||||
|
|
||||||
|
this.toDispose.push(
|
||||||
|
resizeSensor(this.dom.current as HTMLElement, () =>
|
||||||
|
this.editor?.refresh()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// todo 以后优化这个,解决弹窗里面默认光标太小的问题
|
||||||
|
setTimeout(() => this.editor?.refresh(), 350);
|
||||||
|
this.toDispose.push(() => {
|
||||||
|
this.props.editorWillUnMount?.(cm, this.editor!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: CodeMirrorEditorProps) {
|
||||||
|
const props = this.props;
|
||||||
|
|
||||||
|
if (props.value !== prevProps.value) {
|
||||||
|
this.editor && this.setValue(props.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unmounted = true;
|
||||||
|
this.editor?.off('change', this.handleChange);
|
||||||
|
this.toDispose.forEach(fn => fn());
|
||||||
|
this.toDispose = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleChange(editor: any) {
|
||||||
|
this.props.onChange?.(editor.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value?: string) {
|
||||||
|
const doc = this.editor!.getDoc();
|
||||||
|
if (value && value !== doc.getValue()) {
|
||||||
|
const cursor = doc.getCursor();
|
||||||
|
doc.setValue(value);
|
||||||
|
doc.setCursor(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {className} = this.props;
|
||||||
|
return <div className={className} ref={this.dom}></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeMirrorEditor;
|
@ -26,13 +26,14 @@ const collapseStyles: {
|
|||||||
export interface CollapseProps {
|
export interface CollapseProps {
|
||||||
key?: string;
|
key?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
propKey?: string;
|
||||||
mountOnEnter?: boolean;
|
mountOnEnter?: boolean;
|
||||||
unmountOnExit?: boolean;
|
unmountOnExit?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
classPrefix: string;
|
classPrefix: string;
|
||||||
classnames: ClassNamesFn;
|
classnames: ClassNamesFn;
|
||||||
headerPosition?: 'top' | 'bottom';
|
headerPosition?: 'top' | 'bottom';
|
||||||
header?: React.ReactElement;
|
header?: React.ReactNode;
|
||||||
body: any;
|
body: any;
|
||||||
bodyClassName?: string;
|
bodyClassName?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -10,11 +10,12 @@ import Button from './Button';
|
|||||||
|
|
||||||
export interface PickerContainerProps extends ThemeProps, LocaleProps {
|
export interface PickerContainerProps extends ThemeProps, LocaleProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
showTitle?: boolean;
|
||||||
children: (props: {
|
children: (props: {
|
||||||
onClick: (e: React.MouseEvent) => void;
|
onClick: (e: React.MouseEvent) => void;
|
||||||
isOpened: boolean;
|
isOpened: boolean;
|
||||||
}) => JSX.Element;
|
}) => JSX.Element;
|
||||||
popOverRender: (props: {
|
bodyRender: (props: {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
value: any;
|
value: any;
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
@ -85,8 +86,9 @@ export class PickerContainer extends React.Component<
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
popOverRender: dropdownRender,
|
bodyRender: popOverRender,
|
||||||
title,
|
title,
|
||||||
|
showTitle,
|
||||||
translate: __,
|
translate: __,
|
||||||
size
|
size
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@ -103,11 +105,13 @@ export class PickerContainer extends React.Component<
|
|||||||
show={this.state.isOpened}
|
show={this.state.isOpened}
|
||||||
onHide={this.close}
|
onHide={this.close}
|
||||||
>
|
>
|
||||||
<Modal.Header onClose={this.close}>
|
{showTitle !== false ? (
|
||||||
{__(title || 'Select.placeholder')}
|
<Modal.Header onClose={this.close}>
|
||||||
</Modal.Header>
|
{__(title || 'Select.placeholder')}
|
||||||
|
</Modal.Header>
|
||||||
|
) : null}
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{dropdownRender({
|
{popOverRender({
|
||||||
onClose: this.close,
|
onClose: this.close,
|
||||||
value: this.state.value,
|
value: this.state.value,
|
||||||
onChange: this.handleChange
|
onChange: this.handleChange
|
||||||
|
@ -44,7 +44,7 @@ export class TransferPicker extends React.Component<TabsTransferPickerProps> {
|
|||||||
return (
|
return (
|
||||||
<PickerContainer
|
<PickerContainer
|
||||||
title={__('Select.placeholder')}
|
title={__('Select.placeholder')}
|
||||||
popOverRender={({onClose, value, onChange}) => {
|
bodyRender={({onClose, value, onChange}) => {
|
||||||
return <TabsTransfer {...rest} value={value} onChange={onChange} />;
|
return <TabsTransfer {...rest} value={value} onChange={onChange} />;
|
||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -19,18 +19,9 @@ export interface TransferPickerProps extends Omit<TransferProps, 'itemRender'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TransferPicker extends React.Component<TransferPickerProps> {
|
export class TransferPicker extends React.Component<TransferPickerProps> {
|
||||||
@autobind
|
|
||||||
handleClose() {
|
|
||||||
this.setState({
|
|
||||||
inputValue: '',
|
|
||||||
searchResult: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleConfirm(value: any) {
|
handleConfirm(value: any) {
|
||||||
this.props.onChange?.(value);
|
this.props.onChange?.(value);
|
||||||
this.handleClose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -49,12 +40,11 @@ export class TransferPicker extends React.Component<TransferPickerProps> {
|
|||||||
return (
|
return (
|
||||||
<PickerContainer
|
<PickerContainer
|
||||||
title={__('Select.placeholder')}
|
title={__('Select.placeholder')}
|
||||||
popOverRender={({onClose, value, onChange}) => {
|
bodyRender={({onClose, value, onChange}) => {
|
||||||
return <Transfer {...rest} value={value} onChange={onChange} />;
|
return <Transfer {...rest} value={value} onChange={onChange} />;
|
||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
onConfirm={this.handleConfirm}
|
onConfirm={this.handleConfirm}
|
||||||
onCancel={this.handleClose}
|
|
||||||
size={size}
|
size={size}
|
||||||
>
|
>
|
||||||
{({onClick, isOpened}) => (
|
{({onClick, isOpened}) => (
|
||||||
|
261
src/components/formula/Editor.tsx
Normal file
261
src/components/formula/Editor.tsx
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* @file 公式编辑器
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import {uncontrollable} from 'uncontrollable';
|
||||||
|
import {FormulaPlugin, editorFactory} from './plugin';
|
||||||
|
import {doc} from 'amis-formula/dist/doc';
|
||||||
|
import FuncList from './FuncList';
|
||||||
|
import {VariableList} from './VariableList';
|
||||||
|
import {parse} from 'amis-formula';
|
||||||
|
import {autobind} from '../../utils/helper';
|
||||||
|
import CodeMirrorEditor from '../CodeMirror';
|
||||||
|
import {themeable, ThemeProps} from '../../theme';
|
||||||
|
import {localeable, LocaleProps} from '../../locale';
|
||||||
|
|
||||||
|
export interface VariableItem {
|
||||||
|
label: string;
|
||||||
|
value?: string;
|
||||||
|
children?: Array<VariableItem>;
|
||||||
|
selectMode?: 'tree' | 'tabs';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FuncGroup {
|
||||||
|
groupName: string;
|
||||||
|
items: Array<FuncItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FuncItem {
|
||||||
|
name: string;
|
||||||
|
[propName: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormulaEditorProps extends ThemeProps, LocaleProps {
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
value: string;
|
||||||
|
/**
|
||||||
|
* evalMode 即直接就是表达式,否则
|
||||||
|
* 需要 ${这里面才是表达式}
|
||||||
|
* 默认为 true
|
||||||
|
*/
|
||||||
|
evalMode?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于提示的变量集合,默认为空
|
||||||
|
*/
|
||||||
|
variables: Array<VariableItem>;
|
||||||
|
|
||||||
|
variableMode?: 'tabs' | 'tree';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数集合,默认不需要传,即 amis-formula 里面那个函数
|
||||||
|
* 如果有扩充,则需要传。
|
||||||
|
*/
|
||||||
|
functions: Array<FuncGroup>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部标题,默认为表达式
|
||||||
|
*/
|
||||||
|
header: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionsProps {
|
||||||
|
name: string;
|
||||||
|
items: FunctionProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionProps {
|
||||||
|
name: string;
|
||||||
|
intro: string;
|
||||||
|
usage: string;
|
||||||
|
example: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormulaState {
|
||||||
|
focused: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormulaEditor extends React.Component<
|
||||||
|
FormulaEditorProps,
|
||||||
|
FormulaState
|
||||||
|
> {
|
||||||
|
state: FormulaState = {
|
||||||
|
focused: false
|
||||||
|
};
|
||||||
|
editorPlugin?: FormulaPlugin;
|
||||||
|
|
||||||
|
static buildDefaultFunctions(
|
||||||
|
doc: Array<{
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
[propName: string]: any;
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const funcs: Array<FuncGroup> = [];
|
||||||
|
|
||||||
|
doc.forEach(item => {
|
||||||
|
const namespace = item.namespace || 'Others';
|
||||||
|
let exists = funcs.find(item => item.groupName === namespace);
|
||||||
|
if (!exists) {
|
||||||
|
exists = {
|
||||||
|
groupName: namespace,
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
funcs.push(exists);
|
||||||
|
}
|
||||||
|
exists.items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return funcs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps: Pick<
|
||||||
|
FormulaEditorProps,
|
||||||
|
'functions' | 'variables' | 'evalMode'
|
||||||
|
> = {
|
||||||
|
functions: this.buildDefaultFunctions(doc),
|
||||||
|
variables: [],
|
||||||
|
evalMode: true
|
||||||
|
};
|
||||||
|
|
||||||
|
static highlightValue(
|
||||||
|
value: string,
|
||||||
|
variables: Array<VariableItem>,
|
||||||
|
functions: Array<FuncGroup>
|
||||||
|
) {
|
||||||
|
// todo 高亮原始文本
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.editorPlugin?.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleFocus() {
|
||||||
|
this.setState({
|
||||||
|
focused: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleBlur() {
|
||||||
|
this.setState({
|
||||||
|
focused: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
insertValue(value: any, type: 'variable' | 'func') {
|
||||||
|
this.editorPlugin?.insertContent(value, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleEditorMounted(cm: any, editor: any) {
|
||||||
|
this.editorPlugin = new FormulaPlugin(editor, cm, () => this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
validate() {
|
||||||
|
const value = this.props.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
value
|
||||||
|
? parse(value, {
|
||||||
|
evalMode: this.props.evalMode
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
} catch (e) {
|
||||||
|
return e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleFunctionSelect(item: FuncItem) {
|
||||||
|
this.editorPlugin?.insertContent(`${item.name}`, 'func');
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleVariableSelect(item: VariableItem) {
|
||||||
|
this.editorPlugin?.insertContent(
|
||||||
|
{
|
||||||
|
key: item.value,
|
||||||
|
name: item.label
|
||||||
|
},
|
||||||
|
'variable'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleOnChange(value: any) {
|
||||||
|
const onChange = this.props.onChange;
|
||||||
|
onChange?.(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
editorFactory(dom: HTMLElement, cm: any) {
|
||||||
|
return editorFactory(dom, cm, this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
variables,
|
||||||
|
header,
|
||||||
|
value,
|
||||||
|
functions,
|
||||||
|
variableMode,
|
||||||
|
classnames: cx
|
||||||
|
} = this.props;
|
||||||
|
const {focused} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(`FormulaEditor`, {
|
||||||
|
'is-focused': focused
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={cx(`FormulaEditor-header`)}>{header ?? '表达式'}</div>
|
||||||
|
|
||||||
|
<CodeMirrorEditor
|
||||||
|
className={cx('FormulaEditor-editor')}
|
||||||
|
value={value}
|
||||||
|
onChange={this.handleOnChange}
|
||||||
|
editorFactory={this.editorFactory}
|
||||||
|
editorDidMount={this.handleEditorMounted}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
></CodeMirrorEditor>
|
||||||
|
|
||||||
|
<div className={cx('FormulaEditor-settings')}>
|
||||||
|
{Array.isArray(functions) && functions.length ? (
|
||||||
|
<div>
|
||||||
|
<h3>变量</h3>
|
||||||
|
<VariableList
|
||||||
|
className={cx('VariableList')}
|
||||||
|
selectMode={variableMode}
|
||||||
|
data={variables}
|
||||||
|
onSelect={this.handleVariableSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{Array.isArray(variables) && variables.length ? (
|
||||||
|
<div>
|
||||||
|
<h3>函数</h3>
|
||||||
|
<FuncList data={functions} onSelect={this.handleFunctionSelect} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default uncontrollable(
|
||||||
|
themeable(localeable(FormulaEditor)),
|
||||||
|
{
|
||||||
|
value: 'onChange'
|
||||||
|
},
|
||||||
|
['validate']
|
||||||
|
);
|
82
src/components/formula/FuncList.tsx
Normal file
82
src/components/formula/FuncList.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {themeable, ThemeProps} from '../../theme';
|
||||||
|
import Collapse from '../Collapse';
|
||||||
|
import CollapseGroup from '../CollapseGroup';
|
||||||
|
import SearchBox from '../SearchBox';
|
||||||
|
import type {FuncGroup, FuncItem} from './Editor';
|
||||||
|
|
||||||
|
export interface FuncListProps extends ThemeProps {
|
||||||
|
data: Array<FuncGroup>;
|
||||||
|
onSelect?: (item: FuncItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FuncList(props: FuncListProps) {
|
||||||
|
const cx = props.classnames;
|
||||||
|
const [filteredFuncs, setFiteredFuncs] = React.useState(props.data);
|
||||||
|
const [activeFunc, setActiveFunc] = React.useState<any>(null);
|
||||||
|
function onSearch(term: string) {
|
||||||
|
const filtered = props.data
|
||||||
|
.map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
items: term
|
||||||
|
? item.items.filter(item => ~item.name.indexOf(term.toUpperCase()))
|
||||||
|
: item.items
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(item => item.items.length);
|
||||||
|
setFiteredFuncs(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('FormulaFuncList')}>
|
||||||
|
<SearchBox
|
||||||
|
className="FormulaFuncList-searchBox"
|
||||||
|
mini={false}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>
|
||||||
|
<div className={cx('FormulaFuncList-columns')}>
|
||||||
|
<CollapseGroup
|
||||||
|
className="FormulaFuncList-group"
|
||||||
|
defaultActiveKey={filteredFuncs[0]?.groupName}
|
||||||
|
accordion
|
||||||
|
>
|
||||||
|
{filteredFuncs.map(item => (
|
||||||
|
<Collapse
|
||||||
|
headingClassName="FormulaFuncList-groupTitle"
|
||||||
|
bodyClassName="FormulaFuncList-groupBody"
|
||||||
|
propKey={item.groupName}
|
||||||
|
header={item.groupName}
|
||||||
|
key={item.groupName}
|
||||||
|
>
|
||||||
|
{item.items.map(item => (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
`FormulaFuncList-funcItem ${
|
||||||
|
item.name === activeFunc?.name ? 'is-active' : ''
|
||||||
|
}`
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => setActiveFunc(item)}
|
||||||
|
onClick={() => props.onSelect?.(item)}
|
||||||
|
key={item.name}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
))}
|
||||||
|
</CollapseGroup>
|
||||||
|
<div className={cx('FormulaFuncList-column')}>
|
||||||
|
{activeFunc ? (
|
||||||
|
<div className={cx('FormulaFuncList-funcDetail')}>
|
||||||
|
<p>{activeFunc.example}</p>
|
||||||
|
<div>{activeFunc.description}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default themeable(FuncList);
|
86
src/components/formula/Picker.tsx
Normal file
86
src/components/formula/Picker.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import {uncontrollable} from 'uncontrollable';
|
||||||
|
import React from 'react';
|
||||||
|
import {FormulaEditor, FormulaEditorProps} from './Editor';
|
||||||
|
import {autobind} from '../../utils/helper';
|
||||||
|
import PickerContainer from '../PickerContainer';
|
||||||
|
import Editor from './Editor';
|
||||||
|
import ResultBox from '../ResultBox';
|
||||||
|
import {Icon} from '../icons';
|
||||||
|
import {themeable} from '../../theme';
|
||||||
|
import {localeable} from '../../locale';
|
||||||
|
|
||||||
|
export interface FormulaPickerProps extends FormulaEditorProps {
|
||||||
|
// 新的属性?
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边框模式,全边框,还是半边框,或者没边框。
|
||||||
|
*/
|
||||||
|
borderMode?: 'full' | 'half' | 'none';
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormulaPicker extends React.Component<FormulaPickerProps> {
|
||||||
|
@autobind
|
||||||
|
handleConfirm(value: any) {
|
||||||
|
this.props.onChange?.(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
classnames: cx,
|
||||||
|
value,
|
||||||
|
translate: __,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
onChange,
|
||||||
|
size,
|
||||||
|
borderMode,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PickerContainer
|
||||||
|
showTitle={false}
|
||||||
|
bodyRender={({onClose, value, onChange}) => {
|
||||||
|
return <Editor {...rest} value={value} onChange={onChange} />;
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
onConfirm={this.handleConfirm}
|
||||||
|
size={'md'}
|
||||||
|
>
|
||||||
|
{({onClick, isOpened}) => (
|
||||||
|
<ResultBox
|
||||||
|
className={cx(
|
||||||
|
'FormulaPicker',
|
||||||
|
className,
|
||||||
|
isOpened ? 'is-active' : ''
|
||||||
|
)}
|
||||||
|
allowInput={false}
|
||||||
|
result={FormulaEditor.highlightValue(
|
||||||
|
value,
|
||||||
|
rest.variables,
|
||||||
|
rest.functions
|
||||||
|
)}
|
||||||
|
onResultClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
borderMode={borderMode}
|
||||||
|
>
|
||||||
|
<span className={cx('FormulaPicker-icon')}>
|
||||||
|
<Icon icon="pencil" className="icon" />
|
||||||
|
</span>
|
||||||
|
</ResultBox>
|
||||||
|
)}
|
||||||
|
</PickerContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default themeable(
|
||||||
|
localeable(
|
||||||
|
uncontrollable(FormulaPicker, {
|
||||||
|
value: 'onChange'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
49
src/components/formula/VariableList.tsx
Normal file
49
src/components/formula/VariableList.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import GroupedSelection from '../GroupedSelection';
|
||||||
|
import Tabs, {Tab} from '../Tabs';
|
||||||
|
import TreeSelection from '../TreeSelection';
|
||||||
|
import type {VariableItem} from './Editor';
|
||||||
|
|
||||||
|
export interface VariableListProps {
|
||||||
|
className?: string;
|
||||||
|
data: Array<VariableItem>;
|
||||||
|
selectMode?: 'list' | 'tree' | 'tabs';
|
||||||
|
onSelect?: (item: VariableItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariableList({
|
||||||
|
data: list,
|
||||||
|
className,
|
||||||
|
selectMode,
|
||||||
|
onSelect
|
||||||
|
}: VariableListProps) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{selectMode === 'tabs' ? (
|
||||||
|
<Tabs tabsMode="radio">
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<Tab eventKey={index} key={index} title={item.label}>
|
||||||
|
<VariableList
|
||||||
|
selectMode={item.selectMode}
|
||||||
|
data={item.children!}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
) : selectMode === 'tree' ? (
|
||||||
|
<TreeSelection
|
||||||
|
multiple={false}
|
||||||
|
options={list}
|
||||||
|
onChange={(item: any) => onSelect?.(item)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<GroupedSelection
|
||||||
|
multiple={false}
|
||||||
|
options={list}
|
||||||
|
onChange={(item: any) => onSelect?.(item)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
177
src/components/formula/plugin.ts
Normal file
177
src/components/formula/plugin.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* @file 扩展 codemirror
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type CodeMirror from 'codemirror';
|
||||||
|
import {eachTree} from '../../utils/helper';
|
||||||
|
import type {FormulaEditorProps, VariableItem} from './Editor';
|
||||||
|
|
||||||
|
export function editorFactory(
|
||||||
|
dom: HTMLElement,
|
||||||
|
cm: typeof CodeMirror,
|
||||||
|
props: any
|
||||||
|
) {
|
||||||
|
registerLaunguageMode(cm);
|
||||||
|
|
||||||
|
console.log('here', props.evalMode);
|
||||||
|
|
||||||
|
return cm(dom, {
|
||||||
|
value: props.value || '',
|
||||||
|
autofocus: true,
|
||||||
|
mode: props.evalMode ? 'text/formula' : 'text/formula-template'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormulaPlugin {
|
||||||
|
constructor(
|
||||||
|
readonly editor: CodeMirror.Editor,
|
||||||
|
readonly cm: typeof CodeMirror,
|
||||||
|
readonly getProps: () => FormulaEditorProps
|
||||||
|
) {
|
||||||
|
// editor.on('change', this.autoMarkText);
|
||||||
|
this.autoMarkText();
|
||||||
|
}
|
||||||
|
|
||||||
|
autoMarkText() {
|
||||||
|
const {functions, variables, value} = this.getProps();
|
||||||
|
if (value) {
|
||||||
|
// todo functions 也需要自动替换
|
||||||
|
this.autoMark(variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertContent(value: any, type: 'variable' | 'func') {
|
||||||
|
const from = this.editor.getCursor();
|
||||||
|
if (type === 'variable') {
|
||||||
|
this.editor.replaceSelection(value.key);
|
||||||
|
var to = this.editor.getCursor();
|
||||||
|
|
||||||
|
this.markText(from, to, value.name, 'cm-field');
|
||||||
|
} else if (type === 'func') {
|
||||||
|
// todo 支持 snippet,目前是不支持的
|
||||||
|
|
||||||
|
this.editor.replaceSelection(`${value}()`);
|
||||||
|
var to = this.editor.getCursor();
|
||||||
|
this.markText(
|
||||||
|
from,
|
||||||
|
{
|
||||||
|
line: to.line,
|
||||||
|
ch: to.ch - 2
|
||||||
|
},
|
||||||
|
value,
|
||||||
|
'cm-func'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.editor.setCursor({
|
||||||
|
line: to.line,
|
||||||
|
ch: to.ch - 1
|
||||||
|
});
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
this.editor.replaceSelection(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
markText(
|
||||||
|
from: CodeMirror.Position,
|
||||||
|
to: CodeMirror.Position,
|
||||||
|
label: string,
|
||||||
|
className = 'cm-func'
|
||||||
|
) {
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.className = className;
|
||||||
|
text.innerText = label;
|
||||||
|
this.editor.markText(from, to, {
|
||||||
|
atomic: true,
|
||||||
|
replacedWith: text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
autoMark(variables: Array<VariableItem>) {
|
||||||
|
if (!Array.isArray(variables) || !variables.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const varMap: {
|
||||||
|
[propname: string]: string;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
eachTree(
|
||||||
|
variables,
|
||||||
|
item => item.value && (varMap[item.value] = item.label)
|
||||||
|
);
|
||||||
|
const vars = Object.keys(varMap).sort((a, b) => b.length - a.length);
|
||||||
|
|
||||||
|
const editor = this.editor;
|
||||||
|
const lines = editor.lineCount();
|
||||||
|
for (let line = 0; line < lines; line++) {
|
||||||
|
const content = editor.getLine(line);
|
||||||
|
|
||||||
|
// 标记方法调用
|
||||||
|
content.replace(/([A-Z]+)\s*\(/g, (_, func, pos) => {
|
||||||
|
this.markText(
|
||||||
|
{
|
||||||
|
line: line,
|
||||||
|
ch: pos
|
||||||
|
},
|
||||||
|
{
|
||||||
|
line: line,
|
||||||
|
ch: pos + func.length
|
||||||
|
},
|
||||||
|
func,
|
||||||
|
'cm-func'
|
||||||
|
);
|
||||||
|
return _;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标记变量
|
||||||
|
vars.forEach(v => {
|
||||||
|
let from = 0;
|
||||||
|
let idx = -1;
|
||||||
|
while (~(idx = content.indexOf(v, from))) {
|
||||||
|
this.markText(
|
||||||
|
{
|
||||||
|
line: line,
|
||||||
|
ch: idx
|
||||||
|
},
|
||||||
|
{
|
||||||
|
line: line,
|
||||||
|
ch: idx + v.length
|
||||||
|
},
|
||||||
|
varMap[v],
|
||||||
|
'cm-field'
|
||||||
|
);
|
||||||
|
from = idx + v.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {}
|
||||||
|
|
||||||
|
validate() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let modeRegisted = false;
|
||||||
|
function registerLaunguageMode(cm: typeof CodeMirror) {
|
||||||
|
if (modeRegisted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modeRegisted = true;
|
||||||
|
|
||||||
|
// 对应 evalMode
|
||||||
|
cm.defineMode('formula', (config: any, parserConfig: any) => {
|
||||||
|
var formula = cm.getMode(config, 'javascript');
|
||||||
|
if (!parserConfig || !parserConfig.base) return formula;
|
||||||
|
|
||||||
|
return cm.multiplexingMode(cm.getMode(config, parserConfig.base), {
|
||||||
|
open: '${',
|
||||||
|
close: '}',
|
||||||
|
mode: formula
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cm.defineMIME('text/formula', {name: 'formula'});
|
||||||
|
cm.defineMIME('text/formula-template', {name: 'formula', base: 'htmlmixed'});
|
||||||
|
}
|
@ -89,6 +89,7 @@ import './renderers/Form/Select';
|
|||||||
import './renderers/Form/Static';
|
import './renderers/Form/Static';
|
||||||
import './renderers/Form/InputDate';
|
import './renderers/Form/InputDate';
|
||||||
import './renderers/Form/InputDateRange';
|
import './renderers/Form/InputDateRange';
|
||||||
|
import './renderers/Form/InputFormula';
|
||||||
import './renderers/Form/InputRepeat';
|
import './renderers/Form/InputRepeat';
|
||||||
import './renderers/Form/InputTree';
|
import './renderers/Form/InputTree';
|
||||||
import './renderers/Form/TreeSelect';
|
import './renderers/Form/TreeSelect';
|
||||||
|
75
src/renderers/Form/InputFormula.tsx
Normal file
75
src/renderers/Form/InputFormula.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import FormItem, {FormBaseControl, FormControlProps} from './Item';
|
||||||
|
import FormulaPicker from '../../components/formula/Picker';
|
||||||
|
import type {FuncGroup, VariableItem} from '../../components/formula/Editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InputFormula 公式编辑器
|
||||||
|
* 文档:https://baidu.gitee.io/amis/docs/components/form/input-formula
|
||||||
|
*/
|
||||||
|
export interface InputFormulaControlSchema extends FormBaseControl {
|
||||||
|
type: 'input-formula';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* evalMode 即直接就是表达式,否则
|
||||||
|
* 需要 ${这里面才是表达式}
|
||||||
|
* 默认为 true
|
||||||
|
*/
|
||||||
|
evalMode?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于提示的变量集合,默认为空
|
||||||
|
*/
|
||||||
|
variables: Array<VariableItem>;
|
||||||
|
|
||||||
|
variableMode?: 'tabs' | 'tree';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数集合,默认不需要传,即 amis-formula 里面那个函数
|
||||||
|
* 如果有扩充,则需要传。
|
||||||
|
*/
|
||||||
|
functions: Array<FuncGroup>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部标题,默认为表达式
|
||||||
|
*/
|
||||||
|
header: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputFormulaProps
|
||||||
|
extends FormControlProps,
|
||||||
|
Omit<
|
||||||
|
InputFormulaControlSchema,
|
||||||
|
'options' | 'inputClassName' | 'className' | 'descriptionClassName'
|
||||||
|
> {}
|
||||||
|
|
||||||
|
@FormItem({
|
||||||
|
type: 'input-formula'
|
||||||
|
})
|
||||||
|
export class InputFormulaRenderer extends React.Component<InputFormulaProps> {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
selectedOptions,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
evalMode,
|
||||||
|
variables,
|
||||||
|
variableMode,
|
||||||
|
functions,
|
||||||
|
header
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormulaPicker
|
||||||
|
value={selectedOptions}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
evalMode={evalMode}
|
||||||
|
variables={variables}
|
||||||
|
variableMode={variableMode}
|
||||||
|
functions={functions}
|
||||||
|
header={header}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
2
types/uncontrollable/index.d.ts
vendored
2
types/uncontrollable/index.d.ts
vendored
@ -4,5 +4,5 @@ declare module 'uncontrollable' {
|
|||||||
P extends {
|
P extends {
|
||||||
[propName: string]: any;
|
[propName: string]: any;
|
||||||
}
|
}
|
||||||
>(arg: T, config: P): T;
|
>(arg: T, config: P, mapping?: any): T;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user