From da6f941d4a82d450cc9d087f2af57d20c373fa3c Mon Sep 17 00:00:00 2001 From: igrowp <164761965@qq.com> Date: Thu, 20 Oct 2022 12:51:07 +0800 Subject: [PATCH 1/5] =?UTF-8?q?amis-saas-6892=20feat:=20=E9=95=BF=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E5=85=AC=E5=BC=8F=E8=BE=93=E5=85=A5=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Iab086db2c39626d0d4e30679f1ed3d0a45b42225 --- packages/amis-editor/src/index.tsx | 1 + .../textarea-formula/FormulaPicker.tsx | 69 +++ .../TextareaFormulaControl.tsx | 422 ++++++++++++++++++ 3 files changed, 492 insertions(+) create mode 100644 packages/amis-editor/src/renderer/textarea-formula/FormulaPicker.tsx create mode 100644 packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx diff --git a/packages/amis-editor/src/index.tsx b/packages/amis-editor/src/index.tsx index e73ea23c1..7ade1d2aa 100644 --- a/packages/amis-editor/src/index.tsx +++ b/packages/amis-editor/src/index.tsx @@ -149,6 +149,7 @@ import './renderer/ValidationItem'; import './renderer/SwitchMoreControl'; import './renderer/StatusControl'; import './renderer/FormulaControl'; +import './renderer/textarea-formula/TextareaFormulaControl'; import './renderer/DateShortCutControl'; import './renderer/BadgeControl'; import './renderer/style-control/BoxModel'; diff --git a/packages/amis-editor/src/renderer/textarea-formula/FormulaPicker.tsx b/packages/amis-editor/src/renderer/textarea-formula/FormulaPicker.tsx new file mode 100644 index 000000000..1a241a676 --- /dev/null +++ b/packages/amis-editor/src/renderer/textarea-formula/FormulaPicker.tsx @@ -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 = 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 ( + + + 表达式 + + + + + + + + + + ); +}; + +export default FormulaPicker; diff --git a/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx new file mode 100644 index 000000000..fb868e851 --- /dev/null +++ b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx @@ -0,0 +1,422 @@ +/** + * @file 长文本公式输入框 + */ + +import React from 'react'; +import isEqual from 'lodash/isEqual'; +import isString from 'lodash/isString'; +import cx from 'classnames'; +import { + Icon, + isExpression, + render as amisRender, + TooltipWrapper, + FormItem +} from 'amis'; +import {autobind, FormControlProps, Schema} from 'amis-core'; +import {FormulaEditor} from 'amis-ui/lib/components/formula/Editor'; +import FormulaPicker from './FormulaPicker'; + +export interface TextareaFormulaControlProps extends FormControlProps { + height?: number; // 输入框的高度 + + variables?: any; // 公式变量 + + variableMode?: 'tree' | 'tabs'; + + additionalMenus?: Array // 附加底部按钮菜单项 +} + +interface TextareaFormulaControlState { + value: string; // 当前文本值 + + variables: any; // 变量数据 + + menusList: Schema[]; // 底部按钮菜单 + + formulaPickerOpen: boolean; // 是否打开公式编辑器 + + formulaPickerValue: string; // 公式编辑器内容 + + formulaPickerReplaceIdx: number; // 替换表达式的索引,-1代表新增表达式 + + expressionList: string[]; // value中包含的表达式列表 + + cursorStartOffset: number; // 光标偏移量 + + cursorRangeText: string; // 光标所处的文本 +} + +// 用于替换现有表达式 +const REPLACE_KEY = 'TEXTAREA_FORMULA_REPLACE_KEY'; + +export default class TextareaFormulaControl extends React.Component< + TextareaFormulaControlProps, + TextareaFormulaControlState +> { + static defaultProps: Partial = { + variableMode: 'tabs' + }; + + isUnmount: boolean; + + inputRef: any; + + wrapRef: any; + + constructor(props: TextareaFormulaControlProps) { + super(props); + this.state = { + value: '', + variables: [], + menusList: [], + expressionList: [], + formulaPickerOpen: false, + formulaPickerValue: '', + formulaPickerReplaceIdx: -1, + cursorStartOffset: 0, + cursorRangeText: '' + }; + } + + componentDidMount() { + const {additionalMenus = [], value} = this.props; + const menusList = [ + { + type: 'button', + label: '表达式', + onClick: () => { + this.setState({ + formulaPickerOpen: true, + formulaPickerValue: '', + formulaPickerReplaceIdx: -1 + }) + } + } + ]; + this.setState({ + menusList: [...menusList, ...additionalMenus] + }); + this.initExpression(value); + } + + componentDidUpdate(prevProps: TextareaFormulaControlProps) { + // 优先使用props中的变量数据 + if (!this.props.variables) { + // 从amis数据域中取变量数据 + this.resolveVariablesFromScope().then(variables => { + if (Array.isArray(variables)) { + const vars = variables.filter(item => item.children?.length); + if (!this.isUnmount && !isEqual(vars, this.state.variables)) { + this.setState({ + variables: vars + }); + } + } + }); + } + + if (prevProps.value !== this.props.value) { + this.initExpression(this.props.value); + } + } + componentWillUnmount() { + this.isUnmount = true; + } + + 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 + initExpression(value: string) { + let replacedValue = ''; + const expressionList: string[] = []; + if (value && typeof value === 'string') { + // 先把 \${ 转成 \_&{ 方便后面正则处理 + value = value.replace(/\\\${/g, '\\_&{'); + + replacedValue = value.replace(/\${([^{}]*)}/g, (match: string, p1: string) => { + expressionList.push(p1); + return REPLACE_KEY; + }); + replacedValue = replacedValue.replace(/\\_\&{/g, '${'); + } + this.setState({ + expressionList, + value: replacedValue + }); + } + + /** + * 将当前输入框中的值转化成最终的值 + */ + @autobind + revertFinalValue(inputValue: any): any { + // 将 ${xx}(非 \${xx})替换成 \${xx},手动编辑时,自动处理掉 ${xx},避免识别成 公式表达式 + if (inputValue && isString(inputValue) && (isExpression(inputValue) || inputValue.includes('${}'))) { + inputValue = inputValue.replace(/(^|[^\\])\$\{/g, '\\${'); + } + + // 将表达式转化成对应的表达式 + const reg = /
表达式.*?<\/div>/g; + inputValue = inputValue.replace(reg, (match: string) => ( + match.replace(/.*data-expression="(.*?)".*/g, (match: string, p1: string) => p1 ? `\${${p1}}` : '') + )); + return inputValue; + } + + @autobind + closeFormulaPicker() { + this.setState({formulaPickerOpen: false}); + } + + @autobind + handleConfirm(value: any) { + const {formulaPickerReplaceIdx, cursorStartOffset, cursorRangeText} = this.state; + // 去除可能包裹的最外层的${} + value = value.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1); + // 获取焦点 + this.inputRef?.focus(); + // 替换表达式 + if (~formulaPickerReplaceIdx) { + this.replaceExpression(formulaPickerReplaceIdx, value); + } else if (value) { + if (cursorRangeText && this.inputRef) { + let innerHTML = this.inputRef.innerHTML; + const cursorIndex = innerHTML.indexOf(cursorRangeText) + cursorStartOffset; + // 将表达式通过__&[[]]进行包裹,使用${}会被转成\${} + const formula = `__&[[${value}]]`; + // 在光标位置进行添加 + innerHTML = innerHTML.slice(0, cursorIndex) + formula + innerHTML.slice(cursorIndex); + value = this.revertFinalValue(innerHTML); + // 将__&[[]]转化为${} + value = value.replace(/__\&\[\[(.*)\]\]/, (match: string, p1: string) => `\${${p1}}`) + } else { // 添加到最后 + const formula = `\${${value.replace(/^\$\{(.*)\}$/,(match: string, p1: string) => p1)}}`; + + // 多加一个空格避免部分浏览器不能再表达式后面输入的问题 + value = this.props.value + formula + ' '; + } + this.props?.onChange?.(value); + setTimeout(() => { + const selection = getSelection(); + selection?.selectAllChildren(this.inputRef); + selection?.collapseToEnd(); + }); + } + + this.closeFormulaPicker(); + } + + @autobind + handleTextareaBlur(e: React.FocusEvent) { + this.recordLastSelectionRange(); + let inputValue = e.currentTarget.innerHTML?.trim(); + inputValue = inputValue.replace(/(\
)|(\ )|(\ )/g, ''); + + const curValue = this.revertFinalValue(inputValue); + if (curValue !== this.props.value) { + this.props?.onChange?.(curValue); + } + } + + @autobind + handleTextareaKeyDown(e: React.KeyboardEvent) { + // 不支持输入回车键,因为回车键在不同浏览器重表现不同,有的会加上
标签 + if (e.keyCode === 13) { + e.preventDefault(); + } + } + + @autobind + replaceExpression(index: number, value: string = '') { + const {expressionList} = this.state; + expressionList.splice(index, 1, value); + this.setState({expressionList}); + // 组件更新完后再更新value + setTimeout(() => { + const curValue = this.revertFinalValue(this.inputRef.innerHTML); + this.props?.onChange(curValue); + }); + } + + + @autobind + removeExpression(e: React.MouseEvent, idx: number) { + e.stopPropagation(); + this.replaceExpression(idx); + } + + // 记录失焦时的光标位置 + @autobind + recordLastSelectionRange() { + const selection = getSelection(); + const lastEditRange = selection?.getRangeAt(0); + const startContainer: any = lastEditRange?.startContainer; + let cursorStartOffset = 0; + let cursorRangeText = ''; + if (startContainer?.parentNode?.className === 'ae-TextareaResultBox-input') { + cursorStartOffset = lastEditRange?.startOffset || 0; + cursorRangeText = startContainer?.data; + } + + this.setState({ + cursorStartOffset, + cursorRangeText + }); + } + + @autobind + renderExpressionItem(value: string, idx: number) { + const highlightValue = FormulaEditor.highlightValue(value, this.state.variables) || { + html: value + }; + return ( + this.renderFormulaValue(highlightValue) + }} + > +
{ + this.setState({ + formulaPickerOpen: true, + formulaPickerValue: value || '', + formulaPickerReplaceIdx: idx + }) + }} + > + 表达式 + ) => this.removeExpression(e, idx)} + /> +
+
+ ); + } + + @autobind + renderFormulaValue(item: any) { + const html = {__html: item.html}; + // bca-disable-next-line + return ; + } + + @autobind + getTextareaViewValue(value: string, expressionList: string[] = []) { + let replaceStartIdx = value.indexOf(REPLACE_KEY); + let idx = 0; + let result: any = []; + while(~replaceStartIdx) { + const preStr = value.slice(0, replaceStartIdx); + value = value.slice(replaceStartIdx + REPLACE_KEY.length); + replaceStartIdx = value.indexOf(REPLACE_KEY); + if (preStr) { + result.push(preStr); + } + result.push(this.renderExpressionItem(expressionList[idx], idx)); + idx++; + } + if (value) { + result.push(value); + } + return ( + <> + {result.map((item: string | React.ReactNode) => item)} + + ); + } + + render() { + const { + className, + header, + label, + placeholder, + height, + ...rest + } = this.props; + const {value, expressionList, menusList, formulaPickerOpen, formulaPickerValue} = this.state; + + const textareaValues = this.getTextareaViewValue(value, expressionList); + + const variables = rest.variables || this.state.variables || []; + + // 输入框样式 + let resultBoxStyle: {[key in string]: string} = {}; + if (height) { + resultBoxStyle.height = `${height}px`; + } + + return ( +
this.wrapRef = ref}> +
+
+
this.inputRef = ref} + contentEditable + suppressContentEditableWarning + onBlur={this.handleTextareaBlur} + onKeyDown={this.handleTextareaKeyDown} + > + {textareaValues} +
+ {amisRender({ + type: 'dropdown-button', + className: 'ae-TextareaResultBox-dropdown', + menuClassName: 'ae-TextareaResultBox-menus', + popOverContainer: this.wrapRef, + label: '', + level: 'link', + size: 'md', + icon: 'fa fa-plus', + placement: 'top', + trigger: 'hover', + closeOnClick: true, + closeOnOutside: true, + hideCaret: true, + buttons: menusList + })} +
+
+ {formulaPickerOpen ? ( + this.setState({formulaPickerOpen: false})} + onConfirm={this.handleConfirm} + /> + ) : null} +
+ ); + } +} + +@FormItem({ + type: 'ae-textareaFormulaControl' +}) +export class TextareaFormulaControlRenderer extends TextareaFormulaControl {} From 7c110abcc77482a1feaa5e308fadcb6487c59ed4 Mon Sep 17 00:00:00 2001 From: igrowp <164761965@qq.com> Date: Thu, 20 Oct 2022 15:51:04 +0800 Subject: [PATCH 2/5] =?UTF-8?q?amis-saas-7603=20feat:=20=E7=BA=AF=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E5=BC=8F=E8=BE=93=E5=85=A5=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I7e047a08bfeb638d9d8e21a00d7fe9db66b1d5b1 --- packages/amis-editor/src/index.tsx | 1 + .../src/renderer/ExpressionFormulaControl.tsx | 182 ++++++++++++++++++ .../TextareaFormulaControl.tsx | 15 +- 3 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx diff --git a/packages/amis-editor/src/index.tsx b/packages/amis-editor/src/index.tsx index 7ade1d2aa..08cb84258 100644 --- a/packages/amis-editor/src/index.tsx +++ b/packages/amis-editor/src/index.tsx @@ -149,6 +149,7 @@ import './renderer/ValidationItem'; import './renderer/SwitchMoreControl'; import './renderer/StatusControl'; import './renderer/FormulaControl'; +import './renderer/ExpressionFormulaControl'; import './renderer/textarea-formula/TextareaFormulaControl'; import './renderer/DateShortCutControl'; import './renderer/BadgeControl'; diff --git a/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx b/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx new file mode 100644 index 000000000..fe43ada95 --- /dev/null +++ b/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx @@ -0,0 +1,182 @@ +/** + * @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 {renderFormulaValue} from './textarea-formula/TextareaFormulaControl'; +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'; + + +interface ExpressionFormulaControlProps extends FormControlProps { + variables?: any; // 公式变量 + + variableMode?: 'tree' | 'tabs'; +} + +interface ExpressionFormulaControlState { + variables: Array; + + formulaPickerOpen: boolean; + + formulaPickerValue: string; + +} + +export default class ExpressionFormulaControl extends React.Component< + ExpressionFormulaControlProps, + ExpressionFormulaControlState +> { + static defaultProps: Partial = { + 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数据域中取变量数据 + this.resolveVariablesFromScope().then(variables => { + if (Array.isArray(variables)) { + const vars = variables.filter(item => item.children?.length); + if (!this.isUnmount && !isEqual(vars, this.state.variables)) { + this.setState({ + variables: vars + }); + } + } + }); + } + 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 + }); + } + + 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: any) { + value = value.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1); + value = value ? `\${${value}}` : ''; + this.props?.onChange?.(value); + this.setState({ + formulaPickerOpen: false + }); + } + + @autobind + handleClearExpression(e: React.MouseEvent) { + 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 ( +
+ { + formulaPickerValue ? ( + + ) : ( + + ) + } + + {formulaPickerOpen ? ( + this.setState({formulaPickerOpen: false})} + onConfirm={this.handleConfirm} + /> + ) : null} +
+ ) + } +} + +@FormItem({ + type: 'ae-expressionFormulaControl' +}) +export class ExpressionFormulaControlRenderer extends ExpressionFormulaControl {} diff --git a/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx index fb868e851..618677a7a 100644 --- a/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx +++ b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx @@ -17,6 +17,12 @@ import {autobind, FormControlProps, Schema} from 'amis-core'; import {FormulaEditor} from 'amis-ui/lib/components/formula/Editor'; import FormulaPicker from './FormulaPicker'; +export function renderFormulaValue(item: any) { + const html = {__html: item.html}; + // bca-disable-next-line + return ; +} + export interface TextareaFormulaControlProps extends FormControlProps { height?: number; // 输入框的高度 @@ -288,7 +294,7 @@ export default class TextareaFormulaControl extends React.Component< placement="bottom" key={value + idx} tooltip={{ - children: () => this.renderFormulaValue(highlightValue) + children: () => renderFormulaValue(highlightValue) }} >
; - } - @autobind getTextareaViewValue(value: string, expressionList: string[] = []) { let replaceStartIdx = value.indexOf(REPLACE_KEY); From fa51bef367a84532ef2b41af15d1354e3b8d5e74 Mon Sep 17 00:00:00 2001 From: igrowp <164761965@qq.com> Date: Wed, 26 Oct 2022 11:20:59 +0800 Subject: [PATCH 3/5] =?UTF-8?q?amis-saas-6892=20fix:=20=E9=95=BF=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E5=85=AC=E5=BC=8F=E8=BE=93=E5=85=A5=E6=A1=86=E5=9F=BA?= =?UTF-8?q?=E4=BA=8EcodeMirror=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Id04fddb52e9591ba42c66505ea4c00cea3a97122 --- .../src/renderer/ExpressionFormulaControl.tsx | 1 + .../TextareaFormulaControl.tsx | 271 +++--------------- .../src/renderer/textarea-formula/plugin.tsx | 188 ++++++++++++ 3 files changed, 231 insertions(+), 229 deletions(-) create mode 100644 packages/amis-editor/src/renderer/textarea-formula/plugin.tsx diff --git a/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx b/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx index fe43ada95..4e457627b 100644 --- a/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx +++ b/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx @@ -139,6 +139,7 @@ export default class ExpressionFormulaControl extends React.Component< level="primary" tooltip={{ placement: 'bottom', + tooltipClassName: 'btn-configured-tooltip', children: () => renderFormulaValue(highlightValue) }} onClick={this.openFormulaPickerModal} diff --git a/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx index 618677a7a..c355a46f5 100644 --- a/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx +++ b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx @@ -4,24 +4,18 @@ import React from 'react'; import isEqual from 'lodash/isEqual'; -import isString from 'lodash/isString'; import cx from 'classnames'; import { - Icon, - isExpression, render as amisRender, - TooltipWrapper, FormItem } from 'amis'; import {autobind, FormControlProps, Schema} from 'amis-core'; -import {FormulaEditor} from 'amis-ui/lib/components/formula/Editor'; -import FormulaPicker from './FormulaPicker'; +import CodeMirrorEditor from 'amis-ui/lib/components/CodeMirror'; +import {FormulaPlugin, editorFactory} from './plugin'; -export function renderFormulaValue(item: any) { - const html = {__html: item.html}; - // bca-disable-next-line - return ; -} +import FormulaPicker from './FormulaPicker'; +import debounce from 'lodash/debounce'; +import CodeMirror from 'codemirror'; export interface TextareaFormulaControlProps extends FormControlProps { height?: number; // 输入框的高度 @@ -44,19 +38,10 @@ interface TextareaFormulaControlState { formulaPickerValue: string; // 公式编辑器内容 - formulaPickerReplaceIdx: number; // 替换表达式的索引,-1代表新增表达式 - - expressionList: string[]; // value中包含的表达式列表 - - cursorStartOffset: number; // 光标偏移量 - - cursorRangeText: string; // 光标所处的文本 + expressionBrace?: Array; // 表达式所在位置 } -// 用于替换现有表达式 -const REPLACE_KEY = 'TEXTAREA_FORMULA_REPLACE_KEY'; - -export default class TextareaFormulaControl extends React.Component< +export class TextareaFormulaControl extends React.Component< TextareaFormulaControlProps, TextareaFormulaControlState > { @@ -66,22 +51,18 @@ export default class TextareaFormulaControl extends React.Component< isUnmount: boolean; - inputRef: any; - wrapRef: any; + editorPlugin?: FormulaPlugin; + constructor(props: TextareaFormulaControlProps) { super(props); this.state = { value: '', variables: [], menusList: [], - expressionList: [], formulaPickerOpen: false, - formulaPickerValue: '', - formulaPickerReplaceIdx: -1, - cursorStartOffset: 0, - cursorRangeText: '' + formulaPickerValue: '' }; } @@ -95,7 +76,7 @@ export default class TextareaFormulaControl extends React.Component< this.setState({ formulaPickerOpen: true, formulaPickerValue: '', - formulaPickerReplaceIdx: -1 + expressionBrace: undefined }) } } @@ -103,7 +84,6 @@ export default class TextareaFormulaControl extends React.Component< this.setState({ menusList: [...menusList, ...additionalMenus] }); - this.initExpression(value); } componentDidUpdate(prevProps: TextareaFormulaControlProps) { @@ -121,10 +101,6 @@ export default class TextareaFormulaControl extends React.Component< } }); } - - if (prevProps.value !== this.props.value) { - this.initExpression(this.props.value); - } } componentWillUnmount() { this.isUnmount = true; @@ -145,43 +121,14 @@ export default class TextareaFormulaControl extends React.Component< } @autobind - initExpression(value: string) { - let replacedValue = ''; - const expressionList: string[] = []; - if (value && typeof value === 'string') { - // 先把 \${ 转成 \_&{ 方便后面正则处理 - value = value.replace(/\\\${/g, '\\_&{'); - - replacedValue = value.replace(/\${([^{}]*)}/g, (match: string, p1: string) => { - expressionList.push(p1); - return REPLACE_KEY; - }); - replacedValue = replacedValue.replace(/\\_\&{/g, '${'); - } + onExpressionClick(expression: string, brace: Array) { this.setState({ - expressionList, - value: replacedValue + formulaPickerValue: expression, + formulaPickerOpen: true, + expressionBrace: brace }); } - /** - * 将当前输入框中的值转化成最终的值 - */ - @autobind - revertFinalValue(inputValue: any): any { - // 将 ${xx}(非 \${xx})替换成 \${xx},手动编辑时,自动处理掉 ${xx},避免识别成 公式表达式 - if (inputValue && isString(inputValue) && (isExpression(inputValue) || inputValue.includes('${}'))) { - inputValue = inputValue.replace(/(^|[^\\])\$\{/g, '\\${'); - } - - // 将表达式转化成对应的表达式 - const reg = /
表达式.*?<\/div>/g; - inputValue = inputValue.replace(reg, (match: string) => ( - match.replace(/.*data-expression="(.*?)".*/g, (match: string, p1: string) => p1 ? `\${${p1}}` : '') - )); - return inputValue; - } - @autobind closeFormulaPicker() { this.setState({formulaPickerOpen: false}); @@ -189,160 +136,32 @@ export default class TextareaFormulaControl extends React.Component< @autobind handleConfirm(value: any) { - const {formulaPickerReplaceIdx, cursorStartOffset, cursorRangeText} = this.state; - // 去除可能包裹的最外层的${} + const {expressionBrace} = this.state; + // // 去除可能包裹的最外层的${} value = value.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1); - // 获取焦点 - this.inputRef?.focus(); - // 替换表达式 - if (~formulaPickerReplaceIdx) { - this.replaceExpression(formulaPickerReplaceIdx, value); - } else if (value) { - if (cursorRangeText && this.inputRef) { - let innerHTML = this.inputRef.innerHTML; - const cursorIndex = innerHTML.indexOf(cursorRangeText) + cursorStartOffset; - // 将表达式通过__&[[]]进行包裹,使用${}会被转成\${} - const formula = `__&[[${value}]]`; - // 在光标位置进行添加 - innerHTML = innerHTML.slice(0, cursorIndex) + formula + innerHTML.slice(cursorIndex); - value = this.revertFinalValue(innerHTML); - // 将__&[[]]转化为${} - value = value.replace(/__\&\[\[(.*)\]\]/, (match: string, p1: string) => `\${${p1}}`) - } else { // 添加到最后 - const formula = `\${${value.replace(/^\$\{(.*)\}$/,(match: string, p1: string) => p1)}}`; - - // 多加一个空格避免部分浏览器不能再表达式后面输入的问题 - value = this.props.value + formula + ' '; - } - this.props?.onChange?.(value); - setTimeout(() => { - const selection = getSelection(); - selection?.selectAllChildren(this.inputRef); - selection?.collapseToEnd(); - }); - } + 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 - handleTextareaBlur(e: React.FocusEvent) { - this.recordLastSelectionRange(); - let inputValue = e.currentTarget.innerHTML?.trim(); - inputValue = inputValue.replace(/(\
)|(\ )|(\ )/g, ''); - - const curValue = this.revertFinalValue(inputValue); - if (curValue !== this.props.value) { - this.props?.onChange?.(curValue); - } + editorFactory(dom: HTMLElement, cm: any) { + const variables = this.props.variables || this.state.variables; + return editorFactory(dom, cm, {...this.props, variables}); } - @autobind - handleTextareaKeyDown(e: React.KeyboardEvent) { - // 不支持输入回车键,因为回车键在不同浏览器重表现不同,有的会加上
标签 - if (e.keyCode === 13) { - e.preventDefault(); - } - } - - @autobind - replaceExpression(index: number, value: string = '') { - const {expressionList} = this.state; - expressionList.splice(index, 1, value); - this.setState({expressionList}); - // 组件更新完后再更新value - setTimeout(() => { - const curValue = this.revertFinalValue(this.inputRef.innerHTML); - this.props?.onChange(curValue); - }); - } - - - @autobind - removeExpression(e: React.MouseEvent, idx: number) { - e.stopPropagation(); - this.replaceExpression(idx); - } - - // 记录失焦时的光标位置 - @autobind - recordLastSelectionRange() { - const selection = getSelection(); - const lastEditRange = selection?.getRangeAt(0); - const startContainer: any = lastEditRange?.startContainer; - let cursorStartOffset = 0; - let cursorRangeText = ''; - if (startContainer?.parentNode?.className === 'ae-TextareaResultBox-input') { - cursorStartOffset = lastEditRange?.startOffset || 0; - cursorRangeText = startContainer?.data; - } - - this.setState({ - cursorStartOffset, - cursorRangeText - }); - } - - @autobind - renderExpressionItem(value: string, idx: number) { - const highlightValue = FormulaEditor.highlightValue(value, this.state.variables) || { - html: value - }; - return ( - renderFormulaValue(highlightValue) - }} - > -
{ - this.setState({ - formulaPickerOpen: true, - formulaPickerValue: value || '', - formulaPickerReplaceIdx: idx - }) - }} - > - 表达式 - ) => this.removeExpression(e, idx)} - /> -
-
- ); - } - - @autobind - getTextareaViewValue(value: string, expressionList: string[] = []) { - let replaceStartIdx = value.indexOf(REPLACE_KEY); - let idx = 0; - let result: any = []; - while(~replaceStartIdx) { - const preStr = value.slice(0, replaceStartIdx); - value = value.slice(replaceStartIdx + REPLACE_KEY.length); - replaceStartIdx = value.indexOf(REPLACE_KEY); - if (preStr) { - result.push(preStr); - } - result.push(this.renderExpressionItem(expressionList[idx], idx)); - idx++; - } - if (value) { - result.push(value); - } - return ( - <> - {result.map((item: string | React.ReactNode) => item)} - - ); + handleEditorMounted(cm: any, editor: any) { + const variables = this.props.variables || this.state.variables; + this.editorPlugin = new FormulaPlugin(editor, cm, () => ({...this.props, variables}), this.onExpressionClick); } render() { @@ -354,9 +173,7 @@ export default class TextareaFormulaControl extends React.Component< height, ...rest } = this.props; - const {value, expressionList, menusList, formulaPickerOpen, formulaPickerValue} = this.state; - - const textareaValues = this.getTextareaViewValue(value, expressionList); + const {value, menusList, formulaPickerOpen, formulaPickerValue} = this.state; const variables = rest.variables || this.state.variables || []; @@ -370,17 +187,13 @@ export default class TextareaFormulaControl extends React.Component<
this.wrapRef = ref}>
-
this.inputRef = ref} - contentEditable - suppressContentEditableWarning - onBlur={this.handleTextareaBlur} - onKeyDown={this.handleTextareaKeyDown} - > - {textareaValues} -
+ {amisRender({ type: 'dropdown-button', className: 'ae-TextareaResultBox-dropdown', @@ -418,4 +231,4 @@ export default class TextareaFormulaControl extends React.Component< @FormItem({ type: 'ae-textareaFormulaControl' }) -export class TextareaFormulaControlRenderer extends TextareaFormulaControl {} +export default class TextareaFormulaControlRenderer extends TextareaFormulaControl {} diff --git a/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx b/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx new file mode 100644 index 000000000..d02c58387 --- /dev/null +++ b/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx @@ -0,0 +1,188 @@ +/** + * @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 + }); +} + +export class FormulaPlugin { + constructor( + readonly editor: CodeMirror.Editor, + readonly cm: typeof CodeMirror, + readonly getProps: () => TextareaFormulaControlProps, + readonly onExpressionClick: (expression: string, brace: Array) => 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 + ); + } + } + } + + // 计算 `${`、`}` 括号的位置,如 ${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) { + 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 = () => { + this.onExpressionClick(expression, [from, to]); + } + 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() {} +} From 82749c63c50bc6d87c8e00942d6236aa78e63c77 Mon Sep 17 00:00:00 2001 From: igrowp <164761965@qq.com> Date: Wed, 26 Oct 2022 16:37:19 +0800 Subject: [PATCH 4/5] =?UTF-8?q?amis-saas-6892=20fix:=20=E9=95=BF=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E5=85=AC=E5=BC=8F=E8=BE=93=E5=85=A5=E6=A1=86=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=85=A8=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ia0998f0baeaa4c9e4f359e6b87b77fbb4226d953 --- .../src/renderer/ExpressionFormulaControl.tsx | 10 +- .../TextareaFormulaControl.tsx | 91 ++++++++++++------- .../src/renderer/textarea-formula/plugin.tsx | 31 ++++++- 3 files changed, 97 insertions(+), 35 deletions(-) diff --git a/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx b/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx index 4e457627b..4cc8a773a 100644 --- a/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx +++ b/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx @@ -7,7 +7,6 @@ import {autobind, FormControlProps} from 'amis-core'; import cx from 'classnames'; import isEqual from 'lodash/isEqual'; import {FormItem, Button, Icon} from 'amis'; -import {renderFormulaValue} from './textarea-formula/TextareaFormulaControl'; 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'; @@ -83,6 +82,13 @@ export default class ExpressionFormulaControl extends React.Component< }); } + @autobind + renderFormulaValue(item: any) { + const html = {__html: item.html}; + // bca-disable-next-line + return ; + } + async resolveVariablesFromScope() { const {node, manager} = this.props.formProps || this.props; await manager?.getContextSchemas(node); @@ -140,7 +146,7 @@ export default class ExpressionFormulaControl extends React.Component< tooltip={{ placement: 'bottom', tooltipClassName: 'btn-configured-tooltip', - children: () => renderFormulaValue(highlightValue) + children: () => this.renderFormulaValue(highlightValue) }} onClick={this.openFormulaPickerModal} > diff --git a/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx index c355a46f5..68f29ebd2 100644 --- a/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx +++ b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx @@ -6,6 +6,7 @@ import React from 'react'; import isEqual from 'lodash/isEqual'; import cx from 'classnames'; import { + Icon, render as amisRender, FormItem } from 'amis'; @@ -39,6 +40,8 @@ interface TextareaFormulaControlState { formulaPickerValue: string; // 公式编辑器内容 expressionBrace?: Array; // 表达式所在位置 + + isFullscreen: boolean; //是否全屏 } export class TextareaFormulaControl extends React.Component< @@ -46,7 +49,8 @@ export class TextareaFormulaControl extends React.Component< TextareaFormulaControlState > { static defaultProps: Partial = { - variableMode: 'tabs' + variableMode: 'tabs', + height: 100 }; isUnmount: boolean; @@ -62,7 +66,8 @@ export class TextareaFormulaControl extends React.Component< variables: [], menusList: [], formulaPickerOpen: false, - formulaPickerValue: '' + formulaPickerValue: '', + isFullscreen: false }; } @@ -121,7 +126,7 @@ export class TextareaFormulaControl extends React.Component< } @autobind - onExpressionClick(expression: string, brace: Array) { + onExpressionClick(expression: string, brace?: Array) { this.setState({ formulaPickerValue: expression, formulaPickerOpen: true, @@ -137,7 +142,7 @@ export class TextareaFormulaControl extends React.Component< @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); @@ -164,6 +169,13 @@ export class TextareaFormulaControl extends React.Component< this.editorPlugin = new FormulaPlugin(editor, cm, () => ({...this.props, variables}), this.onExpressionClick); } + @autobind + handleFullscreenModeChange() { + this.setState({ + isFullscreen: !this.state.isFullscreen + }); + } + render() { const { className, @@ -173,7 +185,7 @@ export class TextareaFormulaControl extends React.Component< height, ...rest } = this.props; - const {value, menusList, formulaPickerOpen, formulaPickerValue} = this.state; + const {value, menusList, formulaPickerOpen, formulaPickerValue, isFullscreen} = this.state; const variables = rest.variables || this.state.variables || []; @@ -184,32 +196,49 @@ export class TextareaFormulaControl extends React.Component< } return ( -
this.wrapRef = ref}> -
-
- - {amisRender({ - type: 'dropdown-button', - className: 'ae-TextareaResultBox-dropdown', - menuClassName: 'ae-TextareaResultBox-menus', - popOverContainer: this.wrapRef, - label: '', - level: 'link', - size: 'md', - icon: 'fa fa-plus', - placement: 'top', - trigger: 'hover', - closeOnClick: true, - closeOnOutside: true, - hideCaret: true, - buttons: menusList - })} +
this.wrapRef = ref}> +
+ + {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 + })} +
{formulaPickerOpen ? ( diff --git a/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx b/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx index d02c58387..388f35cac 100644 --- a/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx +++ b/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx @@ -22,7 +22,7 @@ export class FormulaPlugin { readonly editor: CodeMirror.Editor, readonly cm: typeof CodeMirror, readonly getProps: () => TextareaFormulaControlProps, - readonly onExpressionClick: (expression: string, brace: Array) => any + readonly onExpressionClick: (expression: string, brace?: Array) => any ) { const {value} = this.getProps(); if (value) { @@ -57,6 +57,32 @@ export class FormulaPlugin { } } + // 找到表达式所在的位置 + 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}[] = []; @@ -155,7 +181,8 @@ export class FormulaPlugin { text.innerText = '表达式'; text.setAttribute('data-expression', expression); text.onclick = () => { - this.onExpressionClick(expression, [from, to]); + const brace = this.getExpressionBrace(expression); + this.onExpressionClick(expression, brace); } const {variables} = this.getProps(); const highlightValue = FormulaEditor.highlightValue(expression, variables) || { From b1a362ebe9877ed0a324b24143a6e253dec655c5 Mon Sep 17 00:00:00 2001 From: igrowp <164761965@qq.com> Date: Tue, 1 Nov 2022 17:47:33 +0800 Subject: [PATCH 5/5] =?UTF-8?q?amis-saas-6892=20fix:=20=E9=95=BF=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E8=BE=93=E5=85=A5=E6=A1=86=E6=94=AF=E6=8C=81=E6=8A=98?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Iaa7a4fb7c46588fd29e87020af0dcacc1c5d031c --- .../src/renderer/ExpressionFormulaControl.tsx | 79 ++++++++++--------- .../TextareaFormulaControl.tsx | 77 ++++++++---------- .../src/renderer/textarea-formula/plugin.tsx | 39 ++++++--- .../src/renderer/textarea-formula/utils.ts | 20 +++++ 4 files changed, 117 insertions(+), 98 deletions(-) create mode 100644 packages/amis-editor/src/renderer/textarea-formula/utils.ts diff --git a/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx b/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx index 4cc8a773a..78cf96c33 100644 --- a/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx +++ b/packages/amis-editor/src/renderer/ExpressionFormulaControl.tsx @@ -10,7 +10,7 @@ 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; // 公式变量 @@ -24,7 +24,6 @@ interface ExpressionFormulaControlState { formulaPickerOpen: boolean; formulaPickerValue: string; - } export default class ExpressionFormulaControl extends React.Component< @@ -43,7 +42,7 @@ export default class ExpressionFormulaControl extends React.Component< variables: [], formulaPickerOpen: false, formulaPickerValue: '' - } + }; } componentDidMount() { @@ -54,12 +53,12 @@ export default class ExpressionFormulaControl extends React.Component< // 优先使用props中的变量数据 if (!this.props.variables) { // 从amis数据域中取变量数据 - this.resolveVariablesFromScope().then(variables => { + const {node, manager} = this.props.formProps || this.props; + resolveVariablesFromScope(node, manager).then(variables => { if (Array.isArray(variables)) { - const vars = variables.filter(item => item.children?.length); - if (!this.isUnmount && !isEqual(vars, this.state.variables)) { + if (!this.isUnmount && !isEqual(variables, this.state.variables)) { this.setState({ - variables: vars + variables: variables }); } } @@ -76,7 +75,8 @@ export default class ExpressionFormulaControl extends React.Component< @autobind initFormulaPickerValue(value: string) { - const formulaPickerValue = value.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1); + const formulaPickerValue = + value?.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1) || ''; this.setState({ formulaPickerValue }); @@ -111,7 +111,7 @@ export default class ExpressionFormulaControl extends React.Component< } @autobind - handleConfirm(value: any) { + handleConfirm(value = '') { value = value.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1); value = value ? `\${${value}}` : ''; this.props?.onChange?.(value); @@ -132,40 +132,41 @@ export default class ExpressionFormulaControl extends React.Component< const {formulaPickerOpen, formulaPickerValue} = this.state; const variables = this.props.variables || this.state.variables; - const highlightValue = FormulaEditor.highlightValue(formulaPickerValue, this.state.variables) || { + const highlightValue = FormulaEditor.highlightValue( + formulaPickerValue, + this.state.variables + ) || { html: formulaPickerValue }; return (
- { - formulaPickerValue ? ( - - ) : ( - - ) - } + {formulaPickerValue ? ( + + ) : ( + + )} {formulaPickerOpen ? ( ) : null}
- ) + ); } } diff --git a/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx index 68f29ebd2..1e0806064 100644 --- a/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx +++ b/packages/amis-editor/src/renderer/textarea-formula/TextareaFormulaControl.tsx @@ -5,11 +5,7 @@ import React from 'react'; import isEqual from 'lodash/isEqual'; import cx from 'classnames'; -import { - Icon, - render as amisRender, - FormItem -} from 'amis'; +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'; @@ -17,6 +13,7 @@ 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; // 输入框的高度 @@ -25,7 +22,7 @@ export interface TextareaFormulaControlProps extends FormControlProps { variableMode?: 'tree' | 'tabs'; - additionalMenus?: Array // 附加底部按钮菜单项 + additionalMenus?: Array; // 附加底部按钮菜单项 } interface TextareaFormulaControlState { @@ -82,7 +79,7 @@ export class TextareaFormulaControl extends React.Component< formulaPickerOpen: true, formulaPickerValue: '', expressionBrace: undefined - }) + }); } } ]; @@ -95,12 +92,12 @@ export class TextareaFormulaControl extends React.Component< // 优先使用props中的变量数据 if (!this.props.variables) { // 从amis数据域中取变量数据 - this.resolveVariablesFromScope().then(variables => { + const {node, manager} = this.props.formProps || this.props; + resolveVariablesFromScope(node, manager).then(variables => { if (Array.isArray(variables)) { - const vars = variables.filter(item => item.children?.length); - if (!this.isUnmount && !isEqual(vars, this.state.variables)) { + if (!this.isUnmount && !isEqual(variables, this.state.variables)) { this.setState({ - variables: vars + variables }); } } @@ -111,20 +108,6 @@ export class TextareaFormulaControl extends React.Component< this.isUnmount = true; } - 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 onExpressionClick(expression: string, brace?: Array) { this.setState({ @@ -150,13 +133,13 @@ export class TextareaFormulaControl extends React.Component< formulaPickerOpen: false, expressionBrace: undefined }); - + this.closeFormulaPicker(); } handleOnChange = debounce((value: any) => { this.props.onChange?.(value); - }, 1000) + }, 1000); @autobind editorFactory(dom: HTMLElement, cm: any) { @@ -166,7 +149,12 @@ export class TextareaFormulaControl extends React.Component< @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); + this.editorPlugin = new FormulaPlugin( + editor, + cm, + () => ({...this.props, variables}), + this.onExpressionClick + ); } @autobind @@ -177,15 +165,14 @@ export class TextareaFormulaControl extends React.Component< } render() { + const {className, header, label, placeholder, height, ...rest} = this.props; const { - className, - header, - label, - placeholder, - height, - ...rest - } = this.props; - const {value, menusList, formulaPickerOpen, formulaPickerValue, isFullscreen} = this.state; + value, + menusList, + formulaPickerOpen, + formulaPickerValue, + isFullscreen + } = this.state; const variables = rest.variables || this.state.variables || []; @@ -196,11 +183,13 @@ export class TextareaFormulaControl extends React.Component< } return ( -
this.wrapRef = ref}> -
+
(this.wrapRef = ref)} + > +
diff --git a/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx b/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx index 388f35cac..79373b65e 100644 --- a/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx +++ b/packages/amis-editor/src/renderer/textarea-formula/plugin.tsx @@ -13,7 +13,8 @@ export function editorFactory( ) { return cm(dom, { value: props.value || '', - autofocus: true + autofocus: true, + lineWrapping: true }); } @@ -22,7 +23,10 @@ export class FormulaPlugin { readonly editor: CodeMirror.Editor, readonly cm: typeof CodeMirror, readonly getProps: () => TextareaFormulaControlProps, - readonly onExpressionClick: (expression: string, brace?: Array) => any + readonly onExpressionClick: ( + expression: string, + brace?: Array + ) => any ) { const {value} = this.getProps(); if (value) { @@ -69,14 +73,16 @@ export class FormulaPlugin { 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 [ + { + line: line, + ch: start - 2 + }, + { + line: line, + ch: end + 1 + } + ]; } } } @@ -148,7 +154,11 @@ export class FormulaPlugin { } } - insertContent(value: any, type?: 'expression', brace?: Array) { + insertContent( + value: any, + type?: 'expression', + brace?: Array + ) { if (brace) { // 替换 const [from, to] = brace; @@ -183,9 +193,12 @@ export class FormulaPlugin { text.onclick = () => { const brace = this.getExpressionBrace(expression); this.onExpressionClick(expression, brace); - } + }; const {variables} = this.getProps(); - const highlightValue = FormulaEditor.highlightValue(expression, variables) || { + const highlightValue = FormulaEditor.highlightValue( + expression, + variables + ) || { html: expression }; // 添加popover diff --git a/packages/amis-editor/src/renderer/textarea-formula/utils.ts b/packages/amis-editor/src/renderer/textarea-formula/utils.ts new file mode 100644 index 000000000..b59c6b53d --- /dev/null +++ b/packages/amis-editor/src/renderer/textarea-formula/utils.ts @@ -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 []; +}