mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-03-02 07:20:38 +00:00
341 lines
9.8 KiB
TypeScript
341 lines
9.8 KiB
TypeScript
import React, { PureComponent } from 'react';
|
||
import PropTypes from 'prop-types';
|
||
import { Select, Balloon, Icon } from '@alife/next';
|
||
import * as acorn from 'acorn';
|
||
|
||
import { isJSExpression, generateI18n } from './locale/utils';
|
||
import zhCN from './locale/zh-CN';
|
||
|
||
import './index.scss';
|
||
|
||
const { Option, AutoComplete } = Select;
|
||
const { Tooltip } = Balloon;
|
||
const helpMap = {
|
||
this: '容器上下文对象',
|
||
'state': '容器的state',
|
||
'props': '容器的props',
|
||
'context': '容器的context',
|
||
'schema': '页面上下文对象',
|
||
'component': '组件上下文对象',
|
||
'constants': '应用常量对象',
|
||
'utils': '应用工具对象',
|
||
'dataSourceMap': '容器数据源Map',
|
||
'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) | undefined;
|
||
|
||
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;
|
||
let nextCursorIndex: number;
|
||
//更新值
|
||
if (actionType === 'itemClick' || actionType === 'enter') {
|
||
let curValue = this.state.value;
|
||
if (curValue) {
|
||
realInputValue = curValue + realInputValue;
|
||
}
|
||
}
|
||
//更新数据源
|
||
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 (/[^\w\.]$/.test(tempStr)) {
|
||
return [];
|
||
} else if (tempStr === null || tempStr === '') {
|
||
return this.getContextKeys([]);
|
||
} 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;
|
||
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: []) {
|
||
const editor = this.props.field.editor;
|
||
console.log(editor);
|
||
const limitKeys = ['schema', 'utils', 'constants'];
|
||
if (keys.length === 0) return limitKeys;
|
||
if (!limitKeys.includes(keys[0])) return [];
|
||
let result = [];
|
||
let keyValue = editor;
|
||
let assert = false;
|
||
keys.forEach(item => {
|
||
if (!keyValue[item] || typeof keyValue[item] !== 'object') {
|
||
assert = true;
|
||
}
|
||
if (keyValue[item]) {
|
||
keyValue = keyValue[item];
|
||
}
|
||
})
|
||
if (assert) return [];
|
||
result = Object.keys(keyValue);
|
||
return result;
|
||
// return utilsKeys.concat(constantsKeys).concat(schemaKeys);
|
||
}
|
||
|
||
/*过滤key */
|
||
filterKey(obj: any, name: string) {
|
||
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(`${name}.${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: string[] = preStr.split('.').slice(-1);
|
||
if (!lastKey) return true;
|
||
if (item.value.indexOf(lastKey) > -1) return true;
|
||
return false;
|
||
}
|
||
|
||
// handleClick = () => {
|
||
// this.props.field.editor.emit('variableBindDialog.open');
|
||
// }
|
||
|
||
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
|
||
) : (
|
||
<div>
|
||
<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>}
|
||
popupClassName="expression-setter-item-inner"
|
||
itemRender={({ value }) => {
|
||
console.log(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)}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
>
|
||
{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: string | any[] = [];
|
||
if (str) keys = str.split('.');
|
||
return keys.slice(0, keys.length - 1);
|
||
}
|
||
/*
|
||
* 设置input组件光标位置在闭合}前
|
||
*/
|
||
setInputCursorPosition(idx: number) {
|
||
this.$input.setSelectionRange(idx, idx);
|
||
this.forceUpdate();
|
||
}
|
||
}
|