mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-03-01 22:10:27 +00:00
376 lines
11 KiB
TypeScript
376 lines
11 KiB
TypeScript
import React, { PureComponent } from 'react';
|
||
import PropTypes from 'prop-types';
|
||
import { Select, Balloon } from '@alife/next';
|
||
import * as acorn from 'acorn';
|
||
|
||
import { isJSExpression, generateI18n } from './locale/utils';
|
||
import zhCN from './locale/zh-CN';
|
||
|
||
const { Option, AutoComplete } = Select;
|
||
const { Tooltip } = Balloon;
|
||
const helpMap = {
|
||
this: '容器上下文对象',
|
||
'this.state': '容器的state',
|
||
'this.props': '容器的props',
|
||
'this.context': '容器的context',
|
||
'this.page': '页面上下文对象',
|
||
'this.component': '组件上下文对象',
|
||
'this.constants': '应用常量对象',
|
||
'this.utils': '应用工具对象',
|
||
'this.dataSourceMap': '容器数据源Map',
|
||
'this.field': '表单Field对象',
|
||
};
|
||
|
||
export default class ExpressionView extends PureComponent {
|
||
static displayName = 'Expression';
|
||
static propTypes = {
|
||
context: PropTypes.object,
|
||
dataSource: PropTypes.array,
|
||
locale: PropTypes.string,
|
||
messages: PropTypes.object,
|
||
onChange: PropTypes.func,
|
||
placeholder: PropTypes.string,
|
||
value: PropTypes.string,
|
||
};
|
||
static defaultProps = {
|
||
context: {},
|
||
dataSource: [],
|
||
locale: 'zh-CN',
|
||
messages: zhCN,
|
||
onChange: () => {},
|
||
placeholder: '',
|
||
value: '',
|
||
};
|
||
expression: React.RefObject<unknown>;
|
||
i18n: any;
|
||
t: void;
|
||
$input: any;
|
||
listenerFun: (event: any) => void;
|
||
|
||
static getInitValue(val: { value: any; match: (arg0: RegExp) => any }) {
|
||
if (isJSExpression(val)) {
|
||
if (typeof val === 'object') {
|
||
return val.value;
|
||
} else if (typeof val === 'string') {
|
||
let arr = val.match(/^\{\{(.*?)\}\}$/);
|
||
if (arr) return arr[1];
|
||
}
|
||
}
|
||
return val;
|
||
}
|
||
constructor(props: Readonly<{}>) {
|
||
super(props);
|
||
this.expression = React.createRef();
|
||
this.i18n = generateI18n(props.locale, props.messages);
|
||
this.state = {
|
||
value: ExpressionView.getInitValue(props.value),
|
||
context: props.context || {},
|
||
dataSource: props.dataSource || [],
|
||
};
|
||
}
|
||
static getDerivedStateFromProps(props: { value: any }, state: { preValue: any }) {
|
||
let curValue = ExpressionView.getInitValue(props.value);
|
||
if (curValue !== state.preValue) {
|
||
return {
|
||
preValue: curValue,
|
||
value: curValue,
|
||
};
|
||
}
|
||
return null;
|
||
}
|
||
onChange(value: string, actionType: string) {
|
||
let realInputValue = value;
|
||
let realDataSource = null;
|
||
const cursorIndex = this.getInputCursorPosition();
|
||
let nextCursorIndex: number;
|
||
//更新值
|
||
if (actionType === 'itemClick' || actionType === 'enter') {
|
||
let curValue = this.state.value;
|
||
if (curValue) {
|
||
//如果是非.结束,则替换当前这个变量;
|
||
let preStr = curValue.substr(0, cursorIndex);
|
||
let nextStr = curValue.substr(cursorIndex);
|
||
let preArr = preStr.split('.');
|
||
let preArrLen = preArr.length;
|
||
let tarPreStr = '';
|
||
if (!preArr[preArrLen - 1]) {
|
||
//如果是.结束,则增加到.后面
|
||
if (preArr[preArrLen - 2] === 'this') {
|
||
preArr = preArr.slice(0, preArrLen - 2);
|
||
preArr.push(value);
|
||
tarPreStr = preArr.join('.');
|
||
} else {
|
||
tarPreStr = preStr + value;
|
||
}
|
||
} else {
|
||
if (preArr[preArrLen - 2] === 'this') {
|
||
preArr = preArr.slice(0, preArrLen - 2);
|
||
} else {
|
||
preArr = preArr.slice(0, preArrLen - 1);
|
||
}
|
||
preArr.push(value);
|
||
tarPreStr = preArr.join('.');
|
||
}
|
||
realInputValue = tarPreStr + nextStr;
|
||
realDataSource = this.getDataSource(tarPreStr + '.') || [];
|
||
nextCursorIndex = tarPreStr.length;
|
||
}
|
||
} else {
|
||
let tarPreStr = value.substr(0, cursorIndex);
|
||
if (tarPreStr) {
|
||
let lastChar = tarPreStr.charAt(tarPreStr.length - 1);
|
||
if (lastChar === '.') {
|
||
realDataSource = this.getDataSource(tarPreStr) || [];
|
||
} else {
|
||
realDataSource = this.getDataSource(tarPreStr + '.');
|
||
}
|
||
} else {
|
||
realDataSource = this.getDataSource('this.');
|
||
}
|
||
}
|
||
//更新数据源
|
||
let newState = {
|
||
value: realInputValue,
|
||
};
|
||
if (realDataSource !== null) newState.dataSource = realDataSource;
|
||
this.setState(newState, () => {
|
||
nextCursorIndex && this.setInputCursorPosition(nextCursorIndex);
|
||
});
|
||
//默认加上变量表达式
|
||
this.t && clearTimeout(this.t);
|
||
this.t = setTimeout(() => {
|
||
const { onChange } = this.props;
|
||
// realInputValue = realInputValue ? `{{${realInputValue}}}` : undefined;
|
||
onChange &&
|
||
onChange({
|
||
type: 'JSExpression',
|
||
value: realInputValue,
|
||
});
|
||
}, 300);
|
||
}
|
||
|
||
/**
|
||
* 获取AutoComplete数据源
|
||
* @param {String}
|
||
* @return {Array}
|
||
*/
|
||
getDataSource(tempStr: string): Array<any> {
|
||
if (tempStr === '' || /[^\w\.]$/.test(tempStr)) {
|
||
return this.getDataSource('this.') || [];
|
||
} else if (/\w\.$/.test(tempStr)) {
|
||
let currentField = this.getCurrentFiled(tempStr);
|
||
if (!currentField) return null;
|
||
let tempKeys = this.getObjectKeys(currentField.str);
|
||
tempKeys = this.getContextKeys(tempKeys);
|
||
if (!tempKeys) return null;
|
||
//给默认情况增加this
|
||
if (tempStr === 'this.') {
|
||
tempKeys = tempKeys.map((item: string) => {
|
||
return 'this.' + item;
|
||
});
|
||
tempKeys.unshift('this');
|
||
}
|
||
return tempKeys;
|
||
} else if (/\.$/.test(tempStr)) {
|
||
return [];
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取光标前的对象字符串,语法解析获取对象字符串
|
||
* @param {String} str 模板字符串
|
||
* @return {String} 光标前的对象字符串
|
||
*/
|
||
getCurrentFiled(str: string | any[]) {
|
||
str += 'x'; //.后面加一个x字符,便于acorn解析
|
||
try {
|
||
let astTree = acorn.parse(str);
|
||
let right = astTree.body[0].expression.right || astTree.body[0].expression;
|
||
if (right.type === 'MemberExpression') {
|
||
let { start, end } = right;
|
||
str = str.slice(start, end);
|
||
return { str, start, end };
|
||
}
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取输入的上下文信息
|
||
* @param {Array}
|
||
* @return {Array}
|
||
*/
|
||
getContextKeys(keys: any) {
|
||
// let context = {};
|
||
// const { appHelper } = this.context;
|
||
// const activeKey = appHelper && appHelper.activeKey;
|
||
// if (!activeKey) return;
|
||
// const activeCtx = appHelper.schemaHelper.compCtxMap && appHelper.schemaHelper.compCtxMap[activeKey];
|
||
// if (!activeCtx) return null;
|
||
// let __self = activeCtx;
|
||
// if (keys && keys.length > 1) {
|
||
// keys.shift(0);
|
||
// let path = '/' + keys.join('/');
|
||
// path = path.replace(/[\[\]]/g, '/');
|
||
// context = jsonuri.get(__self, path);
|
||
// if (context && typeof context === 'object') {
|
||
// return this.filterKey(context);
|
||
// }
|
||
// } else if (keys && keys[0] === 'this') {
|
||
// return this.filterKey(__self);
|
||
// }
|
||
// return null;
|
||
return ['page', 'component'];
|
||
}
|
||
|
||
/*过滤key */
|
||
filterKey(obj: any) {
|
||
let filterKeys = [
|
||
'reloadDataSource',
|
||
'REACT_HOT_LOADER_RENDERED_GENERATION',
|
||
'refs',
|
||
'updater',
|
||
'appHelper',
|
||
'isReactComponent',
|
||
'forceUpdate',
|
||
'setState',
|
||
'isPureReactComponent',
|
||
];
|
||
let result = [];
|
||
for (let key in obj) {
|
||
if (key.indexOf('_') !== 0 && filterKeys.indexOf(key) === -1) {
|
||
result.push(key);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 根据输入项进行筛选
|
||
* @param {String}
|
||
* @param {String}
|
||
* @return {Boolen}
|
||
*/
|
||
filterOption(inputValue: string, item: { value: string | any[] }) {
|
||
const cursorIndex = this.getInputCursorPosition();
|
||
let preStr = inputValue.substr(0, cursorIndex);
|
||
let lastKey = preStr.split('.').slice(-1);
|
||
if (!lastKey) return true;
|
||
if (item.value.indexOf(lastKey) > -1) return true;
|
||
return false;
|
||
}
|
||
|
||
render() {
|
||
const { value, dataSource } = this.state;
|
||
const { placeholder } = this.props;
|
||
const isValObject = !!(value == '[object Object]');
|
||
let title = isValObject
|
||
? this.i18n('valueIllegal')
|
||
: (value || placeholder || this.i18n('jsExpression')).toString();
|
||
const cursorIndex = this.getInputCursorPosition();
|
||
let childNode = cursorIndex ? (
|
||
<div className="cursor-blink">
|
||
{title.substr(0, cursorIndex)}
|
||
<b>|</b>
|
||
{title.substr(cursorIndex)}
|
||
</div>
|
||
) : (
|
||
title
|
||
);
|
||
|
||
return (
|
||
<div ref={this.expression} style={{ width: '100%', display: 'inline-block' }}>
|
||
<Tooltip
|
||
triggerType={isValObject ? ['click'] : ['focus']}
|
||
align="tl"
|
||
popupClassName="code-input-overlay"
|
||
trigger={
|
||
isValObject ? (
|
||
value
|
||
) : (
|
||
<AutoComplete
|
||
{...this.props}
|
||
style={{ width: '100%' }}
|
||
dataSource={dataSource}
|
||
placeholder={placeholder || this.i18n('jsExpression')}
|
||
value={value}
|
||
disabled={isValObject}
|
||
innerBefore={<span style={{ color: '#999', marginLeft: 4 }}>{'{{'}</span>}
|
||
innerAfter={<span style={{ color: '#999', marginRight: 4 }}>{'}}'}</span>}
|
||
itemRender={({ value }) => {
|
||
return (
|
||
<Option key={value} text={value} value={value}>
|
||
<div className="code-input-value">{value}</div>
|
||
<div className="code-input-help">{helpMap[value]}</div>
|
||
</Option>
|
||
);
|
||
}}
|
||
onChange={this.onChange.bind(this)}
|
||
filter={this.filterOption.bind(this)}
|
||
/>
|
||
)
|
||
}
|
||
>
|
||
{childNode}
|
||
</Tooltip>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
componentDidMount() {
|
||
this.$input = this.findInputElement();
|
||
if (this.$input) {
|
||
this.listenerFun = (event) => {
|
||
let isMoveKey = !!(event.type == 'keyup' && ~[37, 38, 39, 91].indexOf(event.keyCode));
|
||
let isMouseup = event.type == 'mouseup';
|
||
if (isMoveKey || isMouseup) {
|
||
let dataSource = this.getDataSource(this.state.value) || [];
|
||
this.setState({
|
||
dataSource,
|
||
});
|
||
}
|
||
};
|
||
this.$input.addEventListener('keyup', this.listenerFun, false);
|
||
this.$input.addEventListener('mouseup', this.listenerFun, false);
|
||
}
|
||
}
|
||
componentWillUnmount() {
|
||
if (this.listenerFun && this.$input) {
|
||
this.$input.removeEventListener('keyup', this.listenerFun, false);
|
||
this.$input.removeEventListener('mouseup', this.listenerFun, false);
|
||
}
|
||
}
|
||
/**
|
||
* 获取Input输入框DOM节点
|
||
*/
|
||
findInputElement() {
|
||
return this.expression.current.children[0].getElementsByTagName('input')[0];
|
||
}
|
||
/**
|
||
* 获取光标位置
|
||
*
|
||
*/
|
||
getInputCursorPosition() {
|
||
if (!this.$input) return;
|
||
return this.$input.selectionStart;
|
||
}
|
||
/*
|
||
* 字符串取得对象keys
|
||
*/
|
||
getObjectKeys(str: string) {
|
||
let keys = [];
|
||
if (str) keys = str.split('.');
|
||
return keys.slice(0, keys.length - 1);
|
||
}
|
||
/*
|
||
* 设置input组件光标位置在闭合}前
|
||
*/
|
||
setInputCursorPosition(idx: number) {
|
||
this.$input.setSelectionRange(idx, idx);
|
||
this.forceUpdate();
|
||
}
|
||
}
|