mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-16 01:40:53 +08:00
Merge changes Iaa7a4fb7,Ia0998f0b,Id04fddb5,I7e047a08,Iab086db2 into fix-fx-formula
* changes: amis-saas-6892 fix: 长文本输入框支持折行 amis-saas-6892 fix: 长文本公式输入框支持全屏 amis-saas-6892 fix: 长文本公式输入框基于codeMirror实现 amis-saas-7603 feat: 纯表达式输入框 amis-saas-6892 feat: 长文本公式输入框
This commit is contained in:
commit
51ef48fb56
@ -149,6 +149,8 @@ import './renderer/ValidationItem';
|
|||||||
import './renderer/SwitchMoreControl';
|
import './renderer/SwitchMoreControl';
|
||||||
import './renderer/StatusControl';
|
import './renderer/StatusControl';
|
||||||
import './renderer/FormulaControl';
|
import './renderer/FormulaControl';
|
||||||
|
import './renderer/ExpressionFormulaControl';
|
||||||
|
import './renderer/textarea-formula/TextareaFormulaControl';
|
||||||
import './renderer/DateShortCutControl';
|
import './renderer/DateShortCutControl';
|
||||||
import './renderer/BadgeControl';
|
import './renderer/BadgeControl';
|
||||||
import './renderer/style-control/BoxModel';
|
import './renderer/style-control/BoxModel';
|
||||||
|
190
packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx
Normal file
190
packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
/**
|
||||||
|
* @file 表达式输入框组件
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {autobind, FormControlProps} from 'amis-core';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import {FormItem, Button, Icon} from 'amis';
|
||||||
|
import FormulaPicker from './textarea-formula/FormulaPicker';
|
||||||
|
import {FormulaEditor} from 'amis-ui/lib/components/formula/Editor';
|
||||||
|
import type {VariableItem} from 'amis-ui/lib/components/formula/Editor';
|
||||||
|
import {resolveVariablesFromScope} from './textarea-formula/utils';
|
||||||
|
|
||||||
|
interface ExpressionFormulaControlProps extends FormControlProps {
|
||||||
|
variables?: any; // 公式变量
|
||||||
|
|
||||||
|
variableMode?: 'tree' | 'tabs';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExpressionFormulaControlState {
|
||||||
|
variables: Array<VariableItem>;
|
||||||
|
|
||||||
|
formulaPickerOpen: boolean;
|
||||||
|
|
||||||
|
formulaPickerValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ExpressionFormulaControl extends React.Component<
|
||||||
|
ExpressionFormulaControlProps,
|
||||||
|
ExpressionFormulaControlState
|
||||||
|
> {
|
||||||
|
static defaultProps: Partial<ExpressionFormulaControlProps> = {
|
||||||
|
variableMode: 'tabs'
|
||||||
|
};
|
||||||
|
|
||||||
|
isUnmount: boolean;
|
||||||
|
|
||||||
|
constructor(props: ExpressionFormulaControlProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
variables: [],
|
||||||
|
formulaPickerOpen: false,
|
||||||
|
formulaPickerValue: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.initFormulaPickerValue(this.props.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: ExpressionFormulaControlProps) {
|
||||||
|
// 优先使用props中的变量数据
|
||||||
|
if (!this.props.variables) {
|
||||||
|
// 从amis数据域中取变量数据
|
||||||
|
const {node, manager} = this.props.formProps || this.props;
|
||||||
|
resolveVariablesFromScope(node, manager).then(variables => {
|
||||||
|
if (Array.isArray(variables)) {
|
||||||
|
if (!this.isUnmount && !isEqual(variables, this.state.variables)) {
|
||||||
|
this.setState({
|
||||||
|
variables: variables
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (prevProps.value !== this.props.value) {
|
||||||
|
this.initFormulaPickerValue(this.props.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.isUnmount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
initFormulaPickerValue(value: string) {
|
||||||
|
const formulaPickerValue =
|
||||||
|
value?.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1) || '';
|
||||||
|
this.setState({
|
||||||
|
formulaPickerValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
renderFormulaValue(item: any) {
|
||||||
|
const html = {__html: item.html};
|
||||||
|
// bca-disable-next-line
|
||||||
|
return <span dangerouslySetInnerHTML={html}></span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveVariablesFromScope() {
|
||||||
|
const {node, manager} = this.props.formProps || this.props;
|
||||||
|
await manager?.getContextSchemas(node);
|
||||||
|
const dataPropsAsOptions = manager?.dataSchema?.getDataPropsAsOptions();
|
||||||
|
|
||||||
|
if (dataPropsAsOptions) {
|
||||||
|
return dataPropsAsOptions.map((item: any) => ({
|
||||||
|
selectMode: 'tree',
|
||||||
|
...item
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
openFormulaPickerModal() {
|
||||||
|
this.setState({
|
||||||
|
formulaPickerOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleConfirm(value = '') {
|
||||||
|
value = value.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1);
|
||||||
|
value = value ? `\${${value}}` : '';
|
||||||
|
this.props?.onChange?.(value);
|
||||||
|
this.setState({
|
||||||
|
formulaPickerOpen: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleClearExpression(e: React.MouseEvent<HTMLElement>) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.props?.onChange?.('');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {value, className, variableMode} = this.props;
|
||||||
|
const {formulaPickerOpen, formulaPickerValue} = this.state;
|
||||||
|
|
||||||
|
const variables = this.props.variables || this.state.variables;
|
||||||
|
const highlightValue = FormulaEditor.highlightValue(
|
||||||
|
formulaPickerValue,
|
||||||
|
this.state.variables
|
||||||
|
) || {
|
||||||
|
html: formulaPickerValue
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('ae-ExpressionFormulaControl', className)}>
|
||||||
|
{formulaPickerValue ? (
|
||||||
|
<Button
|
||||||
|
className="btn-configured"
|
||||||
|
level="primary"
|
||||||
|
tooltip={{
|
||||||
|
placement: 'bottom',
|
||||||
|
tooltipClassName: 'btn-configured-tooltip',
|
||||||
|
children: () => this.renderFormulaValue(highlightValue)
|
||||||
|
}}
|
||||||
|
onClick={this.openFormulaPickerModal}
|
||||||
|
>
|
||||||
|
已配置表达式
|
||||||
|
<Icon
|
||||||
|
icon="close"
|
||||||
|
className="icon"
|
||||||
|
onClick={this.handleClearExpression}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className="btn-set-expression"
|
||||||
|
onClick={this.openFormulaPickerModal}
|
||||||
|
>
|
||||||
|
点击编写表达式
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formulaPickerOpen ? (
|
||||||
|
<FormulaPicker
|
||||||
|
value={formulaPickerValue}
|
||||||
|
initable={true}
|
||||||
|
variables={variables}
|
||||||
|
variableMode={variableMode}
|
||||||
|
evalMode={true}
|
||||||
|
onClose={() => this.setState({formulaPickerOpen: false})}
|
||||||
|
onConfirm={this.handleConfirm}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormItem({
|
||||||
|
type: 'ae-expressionFormulaControl'
|
||||||
|
})
|
||||||
|
export class ExpressionFormulaControlRenderer extends ExpressionFormulaControl {}
|
@ -0,0 +1,69 @@
|
|||||||
|
import React, {useEffect} from 'react';
|
||||||
|
import {Modal, Button} from 'amis';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import Editor from 'amis-ui/lib/components/formula/Editor';
|
||||||
|
|
||||||
|
export interface FormulaPickerProps {
|
||||||
|
onConfirm: (data: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
variables: any[];
|
||||||
|
value?: string;
|
||||||
|
initable?: boolean;
|
||||||
|
variableMode?: 'tabs' | 'tree';
|
||||||
|
evalMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormulaPicker: React.FC<FormulaPickerProps> = props => {
|
||||||
|
const {variables, variableMode, evalMode = true} = props;
|
||||||
|
const [formula, setFormula] = React.useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
const {initable, value} = props;
|
||||||
|
if (initable && value) {
|
||||||
|
setFormula(value);
|
||||||
|
}
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
|
const handleChange = (data: any) => {
|
||||||
|
setFormula(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
props.onClose && props.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
props.onConfirm && props.onConfirm(formula);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className={cx('FormulaPicker-Modal')}
|
||||||
|
size="lg"
|
||||||
|
show
|
||||||
|
onHide={handleClose}
|
||||||
|
closeOnEsc
|
||||||
|
>
|
||||||
|
<Modal.Header onClose={handleClose}>
|
||||||
|
<Modal.Title>表达式</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<Editor
|
||||||
|
header="表达式"
|
||||||
|
variables={variables}
|
||||||
|
variableMode={variableMode}
|
||||||
|
value={formula}
|
||||||
|
evalMode={evalMode}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button onClick={handleClose}>取消</Button>
|
||||||
|
<Button onClick={handleConfirm} level="primary">
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormulaPicker;
|
@ -0,0 +1,248 @@
|
|||||||
|
/**
|
||||||
|
* @file 长文本公式输入框
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import {Icon, render as amisRender, FormItem} from 'amis';
|
||||||
|
import {autobind, FormControlProps, Schema} from 'amis-core';
|
||||||
|
import CodeMirrorEditor from 'amis-ui/lib/components/CodeMirror';
|
||||||
|
import {FormulaPlugin, editorFactory} from './plugin';
|
||||||
|
|
||||||
|
import FormulaPicker from './FormulaPicker';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import CodeMirror from 'codemirror';
|
||||||
|
import {resolveVariablesFromScope} from './utils';
|
||||||
|
|
||||||
|
export interface TextareaFormulaControlProps extends FormControlProps {
|
||||||
|
height?: number; // 输入框的高度
|
||||||
|
|
||||||
|
variables?: any; // 公式变量
|
||||||
|
|
||||||
|
variableMode?: 'tree' | 'tabs';
|
||||||
|
|
||||||
|
additionalMenus?: Array<Schema>; // 附加底部按钮菜单项
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextareaFormulaControlState {
|
||||||
|
value: string; // 当前文本值
|
||||||
|
|
||||||
|
variables: any; // 变量数据
|
||||||
|
|
||||||
|
menusList: Schema[]; // 底部按钮菜单
|
||||||
|
|
||||||
|
formulaPickerOpen: boolean; // 是否打开公式编辑器
|
||||||
|
|
||||||
|
formulaPickerValue: string; // 公式编辑器内容
|
||||||
|
|
||||||
|
expressionBrace?: Array<CodeMirror.Position>; // 表达式所在位置
|
||||||
|
|
||||||
|
isFullscreen: boolean; //是否全屏
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextareaFormulaControl extends React.Component<
|
||||||
|
TextareaFormulaControlProps,
|
||||||
|
TextareaFormulaControlState
|
||||||
|
> {
|
||||||
|
static defaultProps: Partial<TextareaFormulaControlProps> = {
|
||||||
|
variableMode: 'tabs',
|
||||||
|
height: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
isUnmount: boolean;
|
||||||
|
|
||||||
|
wrapRef: any;
|
||||||
|
|
||||||
|
editorPlugin?: FormulaPlugin;
|
||||||
|
|
||||||
|
constructor(props: TextareaFormulaControlProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
value: '',
|
||||||
|
variables: [],
|
||||||
|
menusList: [],
|
||||||
|
formulaPickerOpen: false,
|
||||||
|
formulaPickerValue: '',
|
||||||
|
isFullscreen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {additionalMenus = [], value} = this.props;
|
||||||
|
const menusList = [
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
label: '表达式',
|
||||||
|
onClick: () => {
|
||||||
|
this.setState({
|
||||||
|
formulaPickerOpen: true,
|
||||||
|
formulaPickerValue: '',
|
||||||
|
expressionBrace: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.setState({
|
||||||
|
menusList: [...menusList, ...additionalMenus]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: TextareaFormulaControlProps) {
|
||||||
|
// 优先使用props中的变量数据
|
||||||
|
if (!this.props.variables) {
|
||||||
|
// 从amis数据域中取变量数据
|
||||||
|
const {node, manager} = this.props.formProps || this.props;
|
||||||
|
resolveVariablesFromScope(node, manager).then(variables => {
|
||||||
|
if (Array.isArray(variables)) {
|
||||||
|
if (!this.isUnmount && !isEqual(variables, this.state.variables)) {
|
||||||
|
this.setState({
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.isUnmount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
onExpressionClick(expression: string, brace?: Array<CodeMirror.Position>) {
|
||||||
|
this.setState({
|
||||||
|
formulaPickerValue: expression,
|
||||||
|
formulaPickerOpen: true,
|
||||||
|
expressionBrace: brace
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
closeFormulaPicker() {
|
||||||
|
this.setState({formulaPickerOpen: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleConfirm(value: any) {
|
||||||
|
const {expressionBrace} = this.state;
|
||||||
|
// 去除可能包裹的最外层的${}
|
||||||
|
value = value.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1);
|
||||||
|
value = value ? `\${${value}}` : value;
|
||||||
|
this.editorPlugin?.insertContent(value, 'expression', expressionBrace);
|
||||||
|
this.setState({
|
||||||
|
formulaPickerOpen: false,
|
||||||
|
expressionBrace: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
this.closeFormulaPicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnChange = debounce((value: any) => {
|
||||||
|
this.props.onChange?.(value);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
editorFactory(dom: HTMLElement, cm: any) {
|
||||||
|
const variables = this.props.variables || this.state.variables;
|
||||||
|
return editorFactory(dom, cm, {...this.props, variables});
|
||||||
|
}
|
||||||
|
@autobind
|
||||||
|
handleEditorMounted(cm: any, editor: any) {
|
||||||
|
const variables = this.props.variables || this.state.variables;
|
||||||
|
this.editorPlugin = new FormulaPlugin(
|
||||||
|
editor,
|
||||||
|
cm,
|
||||||
|
() => ({...this.props, variables}),
|
||||||
|
this.onExpressionClick
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleFullscreenModeChange() {
|
||||||
|
this.setState({
|
||||||
|
isFullscreen: !this.state.isFullscreen
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {className, header, label, placeholder, height, ...rest} = this.props;
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
menusList,
|
||||||
|
formulaPickerOpen,
|
||||||
|
formulaPickerValue,
|
||||||
|
isFullscreen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const variables = rest.variables || this.state.variables || [];
|
||||||
|
|
||||||
|
// 输入框样式
|
||||||
|
let resultBoxStyle: {[key in string]: string} = {};
|
||||||
|
if (height) {
|
||||||
|
resultBoxStyle.height = `${height}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('ae-TextareaFormulaControl', {
|
||||||
|
'is-fullscreen': this.state.isFullscreen
|
||||||
|
})}
|
||||||
|
ref={(ref: any) => (this.wrapRef = ref)}
|
||||||
|
>
|
||||||
|
<div className={cx('ae-TextareaResultBox')} style={resultBoxStyle}>
|
||||||
|
<CodeMirrorEditor
|
||||||
|
className="ae-TextareaResultBox-editor"
|
||||||
|
value={value}
|
||||||
|
onChange={this.handleOnChange}
|
||||||
|
editorFactory={this.editorFactory}
|
||||||
|
editorDidMount={this.handleEditorMounted}
|
||||||
|
/>
|
||||||
|
{amisRender({
|
||||||
|
type: 'dropdown-button',
|
||||||
|
className: 'ae-TextareaResultBox-dropdown',
|
||||||
|
menuClassName: 'ae-TextareaResultBox-menus',
|
||||||
|
popOverContainer: this.wrapRef,
|
||||||
|
label: '',
|
||||||
|
level: 'link',
|
||||||
|
size: 'md',
|
||||||
|
icon: 'fa fa-plus',
|
||||||
|
trigger: 'hover',
|
||||||
|
closeOnClick: true,
|
||||||
|
closeOnOutside: true,
|
||||||
|
hideCaret: true,
|
||||||
|
buttons: menusList
|
||||||
|
})}
|
||||||
|
<div className="ae-TextareaResultBox-fullscreen">
|
||||||
|
<a
|
||||||
|
className={cx('Modal-fullscreen')}
|
||||||
|
data-tooltip={isFullscreen ? '退出全屏' : '全屏'}
|
||||||
|
data-position="left"
|
||||||
|
onClick={this.handleFullscreenModeChange}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={isFullscreen ? 'compress-alt' : 'expand-alt'}
|
||||||
|
className="icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{formulaPickerOpen ? (
|
||||||
|
<FormulaPicker
|
||||||
|
value={formulaPickerValue}
|
||||||
|
initable={true}
|
||||||
|
variables={variables}
|
||||||
|
variableMode={rest.variableMode}
|
||||||
|
evalMode={true}
|
||||||
|
onClose={() => this.setState({formulaPickerOpen: false})}
|
||||||
|
onConfirm={this.handleConfirm}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormItem({
|
||||||
|
type: 'ae-textareaFormulaControl'
|
||||||
|
})
|
||||||
|
export default class TextareaFormulaControlRenderer extends TextareaFormulaControl {}
|
228
packages/amis-editor/src/renderer/textarea-formula/plugin.tsx
Normal file
228
packages/amis-editor/src/renderer/textarea-formula/plugin.tsx
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
/**
|
||||||
|
* @file 扩展 codemirror
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type CodeMirror from 'codemirror';
|
||||||
|
import {TextareaFormulaControlProps} from './TextareaFormulaControl';
|
||||||
|
import {FormulaEditor} from 'amis-ui/lib/components/formula/Editor';
|
||||||
|
|
||||||
|
export function editorFactory(
|
||||||
|
dom: HTMLElement,
|
||||||
|
cm: typeof CodeMirror,
|
||||||
|
props: any
|
||||||
|
) {
|
||||||
|
return cm(dom, {
|
||||||
|
value: props.value || '',
|
||||||
|
autofocus: true,
|
||||||
|
lineWrapping: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormulaPlugin {
|
||||||
|
constructor(
|
||||||
|
readonly editor: CodeMirror.Editor,
|
||||||
|
readonly cm: typeof CodeMirror,
|
||||||
|
readonly getProps: () => TextareaFormulaControlProps,
|
||||||
|
readonly onExpressionClick: (
|
||||||
|
expression: string,
|
||||||
|
brace?: Array<CodeMirror.Position>
|
||||||
|
) => any
|
||||||
|
) {
|
||||||
|
const {value} = this.getProps();
|
||||||
|
if (value) {
|
||||||
|
this.autoMark();
|
||||||
|
this.focus(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
autoMark() {
|
||||||
|
const editor = this.editor;
|
||||||
|
const lines = editor.lineCount();
|
||||||
|
for (let line = 0; line < lines; line++) {
|
||||||
|
const content = editor.getLine(line);
|
||||||
|
const braces = this.computedBracesPosition(content);
|
||||||
|
for (let i = 0; i < braces.length; i++) {
|
||||||
|
// 替换每个公式表达式中的内容
|
||||||
|
const start = braces[i].begin;
|
||||||
|
const end = braces[i].end;
|
||||||
|
const expression = content.slice(start, end);
|
||||||
|
this.markExpression(
|
||||||
|
{
|
||||||
|
line: line,
|
||||||
|
ch: start - 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
line: line,
|
||||||
|
ch: end + 1
|
||||||
|
},
|
||||||
|
expression
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到表达式所在的位置
|
||||||
|
getExpressionBrace(expression: string) {
|
||||||
|
const editor = this.editor;
|
||||||
|
const lines = editor.lineCount();
|
||||||
|
for (let line = 0; line < lines; line++) {
|
||||||
|
const content = editor.getLine(line);
|
||||||
|
const braces = this.computedBracesPosition(content);
|
||||||
|
for (let i = 0; i < braces.length; i++) {
|
||||||
|
// 替换每个公式表达式中的内容
|
||||||
|
const start = braces[i].begin;
|
||||||
|
const end = braces[i].end;
|
||||||
|
if (expression === content.slice(start, end)) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
line: line,
|
||||||
|
ch: start - 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
line: line,
|
||||||
|
ch: end + 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算 `${`、`}` 括号的位置,如 ${a}+${b}, 结果是 [ { from: 0, to: 3 }, { from: 5, to: 8 } ]
|
||||||
|
computedBracesPosition(exp: string) {
|
||||||
|
const braces: {begin: number; end: number}[] = [];
|
||||||
|
exp?.replace(/\$\{/g, (val, offset) => {
|
||||||
|
if (val) {
|
||||||
|
const charArr = exp.slice(offset + val.length).split('');
|
||||||
|
const cache = ['${'];
|
||||||
|
|
||||||
|
for (let index = 0; index < charArr.length; index++) {
|
||||||
|
const char = charArr[index];
|
||||||
|
if (char === '$' && charArr[index + 1] === '{') {
|
||||||
|
cache.push('${');
|
||||||
|
} else if (char === '}') {
|
||||||
|
cache.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache.length === 0) {
|
||||||
|
braces.push({begin: offset + 2, end: index + offset + 2});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
return braces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断字符串是否在 ${} 中
|
||||||
|
checkStrIsInBraces(
|
||||||
|
[from, to]: number[],
|
||||||
|
braces: {begin: number; end: number}[]
|
||||||
|
) {
|
||||||
|
let isIn = false;
|
||||||
|
if (braces.length) {
|
||||||
|
for (let index = 0; index < braces.length; index++) {
|
||||||
|
const brace = braces[index];
|
||||||
|
if (from >= brace.begin && to <= brace.end) {
|
||||||
|
isIn = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertBraces(originFrom: CodeMirror.Position, originTo: CodeMirror.Position) {
|
||||||
|
const str = this.editor.getValue();
|
||||||
|
const braces = this.computedBracesPosition(str);
|
||||||
|
|
||||||
|
if (!this.checkStrIsInBraces([originFrom.ch, originTo.ch], braces)) {
|
||||||
|
this.editor.setCursor({
|
||||||
|
line: originFrom.line,
|
||||||
|
ch: originFrom.ch
|
||||||
|
});
|
||||||
|
this.editor.replaceSelection('${');
|
||||||
|
|
||||||
|
this.editor.setCursor({
|
||||||
|
line: originTo.line,
|
||||||
|
ch: originTo.ch + 2
|
||||||
|
});
|
||||||
|
this.editor.replaceSelection('}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertContent(
|
||||||
|
value: any,
|
||||||
|
type?: 'expression',
|
||||||
|
brace?: Array<CodeMirror.Position>
|
||||||
|
) {
|
||||||
|
if (brace) {
|
||||||
|
// 替换
|
||||||
|
const [from, to] = brace;
|
||||||
|
if (type === 'expression') {
|
||||||
|
this.editor.replaceRange(value, from, to);
|
||||||
|
this.autoMark();
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
this.editor.replaceRange(value, from, to);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新增
|
||||||
|
if (type === 'expression') {
|
||||||
|
this.editor.replaceSelection(value);
|
||||||
|
this.autoMark();
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
this.editor.replaceSelection(value);
|
||||||
|
}
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markExpression(
|
||||||
|
from: CodeMirror.Position,
|
||||||
|
to: CodeMirror.Position,
|
||||||
|
expression = '',
|
||||||
|
className = 'cm-expression'
|
||||||
|
) {
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.className = className;
|
||||||
|
text.innerText = '表达式';
|
||||||
|
text.setAttribute('data-expression', expression);
|
||||||
|
text.onclick = () => {
|
||||||
|
const brace = this.getExpressionBrace(expression);
|
||||||
|
this.onExpressionClick(expression, brace);
|
||||||
|
};
|
||||||
|
const {variables} = this.getProps();
|
||||||
|
const highlightValue = FormulaEditor.highlightValue(
|
||||||
|
expression,
|
||||||
|
variables
|
||||||
|
) || {
|
||||||
|
html: expression
|
||||||
|
};
|
||||||
|
// 添加popover
|
||||||
|
const popoverEl = document.createElement('div');
|
||||||
|
// bca-disable-next-line
|
||||||
|
popoverEl.innerHTML = highlightValue.html;
|
||||||
|
popoverEl.classList.add('expression-popover');
|
||||||
|
text.appendChild(popoverEl);
|
||||||
|
|
||||||
|
this.editor.markText(from, to, {
|
||||||
|
atomic: true,
|
||||||
|
replacedWith: text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 焦点放在最后
|
||||||
|
focus(value: string) {
|
||||||
|
this.editor.setCursor({
|
||||||
|
line: 0,
|
||||||
|
ch: value?.length || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {}
|
||||||
|
|
||||||
|
validate() {}
|
||||||
|
}
|
20
packages/amis-editor/src/renderer/textarea-formula/utils.ts
Normal file
20
packages/amis-editor/src/renderer/textarea-formula/utils.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* 从amis数据域中取变量数据
|
||||||
|
* @param node
|
||||||
|
* @param manager
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function resolveVariablesFromScope(node: any, manager: any) {
|
||||||
|
await manager?.getContextSchemas(node);
|
||||||
|
const dataPropsAsOptions = manager?.dataSchema?.getDataPropsAsOptions();
|
||||||
|
|
||||||
|
if (dataPropsAsOptions) {
|
||||||
|
return dataPropsAsOptions
|
||||||
|
.map((item: any) => ({
|
||||||
|
selectMode: 'tree',
|
||||||
|
...item
|
||||||
|
}))
|
||||||
|
.filter((item: any) => item.children?.length);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user