Merge branch 'v/0.8.0' of gitlab.alibaba-inc.com:ali-lowcode/ali-lowcode-engine into v/0.8.0

This commit is contained in:
kangwei 2020-03-30 17:09:31 +08:00
commit bced181e98
17 changed files with 2626 additions and 0 deletions

View File

@ -0,0 +1,35 @@
// color-setter
.lowcode-color-box {
margin-right: -5px;
padding: 3px 0 3px 3px;
width: 26px;
height: 26px;
display: inline-block;
div {
width: 20px;
height: 20px;
border: 1px solid #ddd;
}
}
.next-balloon-normal.lowcode-color-content {
padding: 0;
background: #ffffff;
border-radius: 0;
border: 1px solid #e5e5e5;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
&:after {
display: none;
}
.sketch-picker {
border-radius: 0 !important;
border: none !important;
box-shadow: none !important;
.flexbox-fix {
input {
width: 100% !important;
min-width: 30px;
text-align: center;
}
}
}
}

View File

@ -0,0 +1,99 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { SketchPicker } from 'react-color';
import { Input, Balloon } from '@alife/next';
import './index.scss';
interface Color {
rgb: any;
onChange: function;
}
export interface PluginProps {
value: string;
onChange: any;
}
export default class ColorPickerView extends PureComponent<PluginProps> {
static display = 'ColorPicker';
static propTypes = {
onChange: PropTypes.func,
value: PropTypes.string
};
static defaultProps = {
onChange: () => {},
value: ''
};
constructor(props: Readonly<{value: string; defaultValue: string}>) {
super(props);
this.state = {
value: props.value || props.defaultValue
};
}
static getDerivedStateFromProps(props: { value: string; }, state: { preValue: string; }) {
if (props.value != state.preValue) {
return {
preValue: props.value,
value: props.value
};
}
return null;
}
onChangeComplete = (color: Color): void => {
let value;
if (color.rgb.a < 1) {
let rgb = color.rgb;
let rgba = [rgb.r, rgb.g, rgb.b, rgb.a];
value = `rgba(${rgba.join(',')})`;
} else {
value = color.hex;
}
this.setState({
value
});
this.props.onChange && this.props.onChange(value);
}
onInputChange = (value: string): void => {
if (/^[0-9a-zA-Z]{6}$/.test(value)) value = '#' + value;
this.setState({
value
});
this.props.onChange && this.props.onChange(value);
}
render(): React.ReactNode {
const { value, onChange, ...restProps } = this.props;
let boxStyle = {
backgroundColor: this.state.value
};
let triggerNode = (
<div className="lowcode-color-box">
<div style={boxStyle} />
</div>
);
let InnerBeforeNode = (
<Balloon
className={'lowcode-color-content'}
trigger={triggerNode}
needAdjust={true}
triggerType="click"
closable={false}
alignEdge="edge"
offset={[-3, -6]}
>
<SketchPicker
onChangeComplete={this.onChangeComplete}
color={this.state.value}
arrowPointAtCenter={true}
/>
</Balloon>
);
return (
<Input
{...restProps}
innerBefore={InnerBeforeNode}
onChange={this.onInputChange}
value={this.state.value}
/>
);
}
}

View File

@ -0,0 +1,130 @@
// mixin
.lowcode-setter-mixin > * {
vertical-align: middle;
}
.lowcode-setter-mixin {
width: 86%;
}
.lowcode-setter-mixin .next-input {
width: 100%;
}
.lowcode-setter-mixin .next-select-trigger {
width: 100%;
}
// json-setter
// :global {
.nrs-monaco-form {
.next-form-item:last-child {
margin: 0 !important;
}
}
.monaco-editor-wrap {
.luna-monaco-button .next-icon-first {
height: 26px;
}
.monaco_fullscreen_icon {
position: absolute;
line-height: 1;
z-index: 7;
color: #ddd;
&:hover {
color: #fff;
}
}
.btns-eare {
text-align: left;
line-height: initial;
margin-top: 5px;
// button{
// margin-right: 10px;
// }
}
&.monaco-nofullscreen {
position: relative !important;
.monaco_fullscreen_icon {
position: absolute;
top: 5px;
right: 5px;
line-height: 1;
z-index: 7;
i:before {
font-size: 16px;
}
}
}
&.monaco-fullscreen {
position: fixed !important;
height: 100% !important;
width: 100% !important;
border: 0;
margin: 0;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1001;
overflow: hidden;
.monaco_fullscreen_icon {
top: 10px;
right: 10px;
i:before {
font-size: 24px;
}
}
}
}
.luna-monaco-button button {
width: 100%;
}
.luna-monaco-button-dialog {
.next-dialog-body {
padding: 0;
.next-form-item {
height: 100%;
margin-bottom: 0;
.next-form-item-control,
.next-form-item-control > div {
height: 100% !important;
}
.next-form-item-help {
position: absolute;
}
}
}
}
// }
// color-setter
.lowcode-color-box {
margin-right: -5px;
padding: 3px 0 3px 3px;
width: 26px;
height: 26px;
display: inline-block;
div {
width: 20px;
height: 20px;
border: 1px solid #ddd;
}
}
.next-balloon-normal.lowcode-color-content {
padding: 0;
background: #ffffff;
border-radius: 0;
border: 1px solid #e5e5e5;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
&:after {
display: none;
}
.sketch-picker {
border-radius: 0 !important;
border: none !important;
box-shadow: none !important;
.flexbox-fix {
input {
width: 100% !important;
min-width: 30px;
text-align: center;
}
}
}
}

View File

@ -0,0 +1,377 @@
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();
}
}

View File

@ -0,0 +1,242 @@
export default [
{
label: 'constants',
kind: 'Class',
insertText: 'constants',
detail: '应用全局常量',
documentation: '应用范围定义的通用常量'
},
{
label: 'utils',
kind: 'Class',
insertText: 'utils',
detail: '应用全局公共函数',
documentation: '应用范围扩展的公共函数'
},
{
label: 'state',
kind: 'Enum',
insertText: 'state',
detail: '当前所在容器组件内部状态',
documentation: 'React Class内部状态state'
},
{
label: 'setState',
kind: 'Function',
insertText: 'setState({\n\t$0\n})',
insertTextRules: 'InsertAsSnippet',
detail: '设置当前所在容器组件的state数据',
documentation: '原生React方法会自动更新组件视图'
},
{
label: 'reloadDataSource',
kind: 'Function',
insertText: 'reloadDataSource(${1:${2:namespace}, ${3:false}, ${4:callback}})',
insertTextRules: 'InsertAsSnippet',
detail: '刷新当前所在的容器组件',
documentation: '触发当前所在的容器组件,重新发送异步请求,并用最新数据更新视图'
},
{
label: 'location',
kind: 'Class',
insertText: 'location',
detail: '路由解析对象'
},
{
label: 'location.query',
kind: 'Value',
insertText: 'location.query.${1:xxxx}',
insertTextRules: 'InsertAsSnippet',
detail: '从路由解析对象中获取参数信息'
},
{
label: 'history',
kind: 'Class',
insertText: 'history',
detail: '路由历史对象'
},
{
label: 'React',
kind: 'Keyword',
insertText: 'React',
detail: 'React对象'
},
{
label: 'ReactDOM',
kind: 'Keyword',
insertText: 'ReactDOM',
detail: 'ReactDom对象'
},
{
label: 'ReactDOM.findDOMNode',
kind: 'Function',
insertText: 'ReactDOM.findDOMNode(${1:this.refs.xxxx})',
insertTextRules: 'InsertAsSnippet',
detail: 'ReactDom查找真实dom node'
},
{
label: 'Dialog.alert',
kind: 'Method',
insertText: [
'Dialog.alert({',
"\tcontent: '${1:Alert content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: 'alert弹框 By Fusion'
},
{
label: 'Dialog.confirm',
kind: 'Method',
insertText: [
'Dialog.confirm({',
"\tcontent: '${1:Confirm content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '确认弹出框 By Fusion'
},
{
label: 'Message.success',
kind: 'Method',
insertText: 'Message.success(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '成功反馈提示 By Fusion'
},
{
label: 'Message.error',
kind: 'Method',
insertText: 'Message.error(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '错误反馈提示 By Fusion'
},
{
label: 'Message.help',
kind: 'Method',
insertText: 'Message.help(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '帮助反馈提示 By Fusion'
},
{
label: 'Message.loading',
kind: 'Method',
insertText: 'Message.loading(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: 'loading反馈提示 By Fusion'
},
{
label: 'Message.notice',
kind: 'Method',
insertText: 'Message.notice(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '注意反馈提示 By Fusion'
},
{
label: 'Message.waining',
kind: 'Method',
insertText: 'Message.waining(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '警告反馈提示 By Fusion'
},
{
label: 'Modal.confirm',
kind: 'Method',
insertText: [
'Modal.confirm({',
"\tcontent: '${1:Confirm content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '确认弹出框 By Antd'
},
{
label: 'Modal.info',
kind: 'Method',
insertText: [
'Modal.info({',
"\tcontent: '${1:Info content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '信息弹出框 By Antd'
},
{
label: 'Modal.success',
kind: 'Method',
insertText: [
'Modal.success({',
"\tcontent: '${1:Success content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '成功弹出框 By Antd'
},
{
label: 'Modal.error',
kind: 'Method',
insertText: [
'Modal.error({',
"\tcontent: '${1:Error content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '错误弹出框 By Antd'
},
{
label: 'Modal.warning',
kind: 'Method',
insertText: [
'Modal.warning({',
"\tcontent: '${1:Warning content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '警告弹出框 By Antd'
}
];

View File

@ -0,0 +1,21 @@
import IntlMessageFormat from 'intl-messageformat';
export const isJSExpression = (obj = '') => {
if(obj && typeof obj === 'object' && obj.type === 'JSExpression') {
return true;
}
return false;
}
/**
* 用于构造国际化字符串处理函数
* @param {*} locale 国际化标识例如 zh-CNen-US
* @param {*} messages 国际化语言包
*/
export const generateI18n = (locale = 'zh-CN', messages = {}) => {
return function (key, values = {}) {
if (!messages || !messages[key]) return '';
const formater = new IntlMessageFormat(messages[key], locale);
return formater.format(values);
};
}

View File

@ -0,0 +1,36 @@
export default {
// function
setting: '点击设置',
edit: '编辑',
submitConfirm: '确认提交 cmd+s',
close: '关闭 esc',
fullScreen: '全屏',
cancelFullScreen: '取消全屏',
jsonIllegal: '非json格式',
functionIllegal: '非function格式',
objectIllegal: '非object格式',
circularRef: '对象中出现循环引用的对象',
formatError: '格式错误',
saved: '已保存',
// expression
valueIllegal: '值类型为对象类型,与当前组件属性设置的控件类型不匹配,请在属性“代码编辑模式”下进行编辑',
jsExpression: '请输入JS表达式',
// Mixin
input: '字符串Input',
textarea: '多行字符串Textarea',
expression: '变量控件Expression',
monacoEditor: '编辑器MonacoEditor',
numberPicker: '数字NumberPicker',
bool: '布尔Switch',
datePicker: '日期选择DatePicker',
select: '下拉选择Select',
radio: '单项选择RadioGroup',
date: '日期选择DatePicker',
dateYear: '年选择DatePicker',
dateMonth: '月选择DatePicker',
dateRange: '日期区间选择DatePicker',
list: '数组List',
object: '对象ObjectButton',
reactNode: '节点类型ReactNode',
typeError: 'Minix组件属性Types配置错误存在不支持类型[{type}],请检查组件属性配置',
};

View File

@ -0,0 +1,83 @@
// json-setter
// :global {
.nrs-monaco-form {
.next-form-item:last-child {
margin: 0 !important;
}
}
.monaco-editor-wrap {
.luna-monaco-button .next-icon-first {
height: 26px;
}
.monaco_fullscreen_icon {
position: absolute;
line-height: 1;
z-index: 7;
color: #ddd;
&:hover {
color: #fff;
}
}
.btns-eare {
text-align: left;
line-height: initial;
margin-top: 5px;
// button{
// margin-right: 10px;
// }
}
&.monaco-nofullscreen {
position: relative !important;
.monaco_fullscreen_icon {
position: absolute;
top: 5px;
right: 5px;
line-height: 1;
z-index: 7;
i:before {
font-size: 16px;
}
}
}
&.monaco-fullscreen {
position: fixed !important;
height: 100% !important;
width: 100% !important;
border: 0;
margin: 0;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1001;
overflow: hidden;
.monaco_fullscreen_icon {
top: 10px;
right: 10px;
i:before {
font-size: 24px;
}
}
}
}
.luna-monaco-button button {
width: 100%;
}
.luna-monaco-button-dialog {
.next-dialog-body {
padding: 0;
.next-form-item {
height: 100%;
margin-bottom: 0;
.next-form-item-control,
.next-form-item-control > div {
height: 100% !important;
}
.next-form-item-help {
position: absolute;
}
}
}
}
// }

View File

@ -0,0 +1,573 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { js_beautify, css_beautify } from 'js-beautify';
import MonacoEditor from 'react-monaco-editor';
import classNames from 'classnames';
import { Icon, Message } from '@alife/next';
import ObjectButton from '@ali/iceluna-comp-object-button';
import FormItem from '@ali/iceluna-comp-form/lib/item';
import { serialize, jsonuri, generateI18n } from '@ali/iceluna-sdk/lib/utils';
import localeConfig from '@ali/iceluna-sdk/lib/hoc/localeConfig';
import Snippets from './locale/snippets';
import zhCN from './locale/zh-CN';
import './index.scss';
let registerApiAndSnippetStatus = false; //判断注册api机制
window.bt = js_beautify;
class MonacoEditorView extends PureComponent {
static displayName = 'MonacoEditor';
render() {
const { type, ...restProps } = this.props;
let Node = type == 'button' ? MonacoEditorButtonView : MonacoEditorDefaultView;
return <Node {...restProps} registerApi={apis => Object.assign(this, apis)} />;
}
}
localeConfig('MonacoEditor', MonacoEditorView);
//monaco编辑器存在3种主题vs、vs-dark、hc-black
class MonacoEditorDefaultView extends PureComponent {
static displayName = 'MonacoEditorDefault';
static propTypes = {
locale: PropTypes.string,
messages: PropTypes.object
};
static defaultProps = {
locale: 'zh-CN',
messages: zhCN,
width: '100%',
height: '300px',
language: 'javascript',
autoFocus: false, //自动获得焦点
autoSubmit: true, //自动提交
placeholder: '', //默认占位内容
btnText: '提交',
btnSize: 'small',
rules: [], //校验规则
options: {
readOnly: false,
automaticLayout: true,
folding: true, //默认开启折叠代码功能
lineNumbers: 'on',
wordWrap: 'off',
formatOnPaste: true,
fontSize: 12,
tabSize: 2,
scrollBeyondLastLine: false,
fixedOverflowWidgets: false,
snippetSuggestions: 'top',
minimap: {
enabled: true
},
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
verticalScrollbarSize: 0
}
}
};
strValue: string;
i18n: any;
editorRef: React.RefObject<unknown>;
options: any;
fullScreenOptions: any;
position: any;
editor: any;
editorNode: unknown;
editorParentNode: any;
constructor(props: Readonly<{}>) {
super(props);
this.strValue = '';
this.i18n = generateI18n(props.locale, props.messages);
this.editorRef = React.createRef();
this.options = Object.assign({}, MonacoEditorDefaultView.defaultProps.options, props.options);
this.fullScreenOptions = {
...this.options,
lineNumbers: 'on',
folding: true,
scrollBeyondLastLine: true,
minimap: {
enabled: true
}
};
this.state = {
isFullScreen: false
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.fullScreen = this.fullScreen.bind(this);
this.format = this.format.bind(this);
}
componentDidUpdate() {
//如果是全屏操作,获得焦点,光标保留在原来位置;
if (this.position) {
this.editor.focus();
this.editor.setPosition(this.position);
delete this.position;
}
}
componentDidMount() {
this.editorNode = this.editorRef.current; //记录当前dom节点
this.editorParentNode = this.editorNode.parentNode; //记录父节点;
//自动获得焦点, 格式化需要时间
if (this.props.autoFocus) {
setTimeout(() => {
this.editor.setPosition({
column: 4,
lineNumber: 2
});
this.editor.focus();
}, 100);
}
//快捷键编码
let CtrlCmd = 2048;
let KEY_S = 49;
let Shift = 1024;
let KEY_F = 36;
let KEY_B = 32;
let Escape = 9;
this.editor.addCommand(CtrlCmd | KEY_S, () => {
this.onSubmit(); //保存快捷键
});
this.editor.addCommand(CtrlCmd | Shift | KEY_F, () => {
this.fullScreen(); //全屏快捷键
});
this.editor.addCommand(CtrlCmd | KEY_B, () => {
this.format(); //美化快捷键
});
this.editor.addCommand(Escape, () => {
this.props.onEscape && this.props.onEscape();
});
//注册api
this.editor.submit = this.onSubmit;
this.editor.format = this.format;
this.editor.fullScreen = this.fullScreen;
this.editor.toJson = this.toJson;
this.editor.toObject = this.toObject;
this.editor.toFunction = this.toFunction;
//针对object情况改写setValue和getValue api
if (this.props.language === 'object') {
let getValue = this.editor.getValue;
let setValue = this.editor.setValue;
this.editor.getValue = () => {
return getValue.call(this.editor).substring(this.valuePrefix.length);
};
this.editor.setValue = value => {
return setValue.call(this.editor, [this.valuePrefix + value]);
};
}
}
render() {
const {
value,
placeholder,
style,
className,
width,
height,
language,
theme,
editorWillMount,
editorDidMount,
registerApi
} = this.props;
const { isFullScreen } = this.state;
this.valuePrefix = ''; //值前缀
if (language === 'object') this.valuePrefix = 'export default ';
if (!this.isFullScreenAction) {
//将值转换成目标值
let nowValue = this.valueHandler(value || placeholder, language);
let curValue = this.valueHandler(this.strValue, language);
if (nowValue !== curValue) this.strValue = nowValue;
if (language === 'object') this.strValue = this.strValue || placeholder || '{\n\t\n}'; //设置初始化值
if (language === 'json' && this.strValue === '{}') this.strValue = '{\n\t\n}';
}
this.isFullScreenAction = false;
//真实高亮语言
let tarLanguage = language;
if (language === 'object' || language === 'function') {
tarLanguage = 'javascript';
}
let classes = classNames('monaco-editor-wrap', {
['monaco-fullscreen']: !!isFullScreen,
['monaco-nofullscreen']: !isFullScreen
});
let tarStyle = Object.assign({ minHeight: 60, width, height }, style);
return (
<div className={className} style={tarStyle}>
<div ref={this.editorRef} style={{ height: '100%' }} className={classes}>
<MonacoEditor
value={this.valuePrefix + this.strValue}
width="100%"
height="300"
language={tarLanguage}
theme={theme || window.__monacoTheme || 'vs-dark'}
options={isFullScreen ? this.fullScreenOptions : this.options}
onChange={this.onChange}
editorWillMount={editorWillMount}
editorDidMount={(editor, monaco) => {
this.editor = editor;
registerApi({ editor });
this.registerApiAndSnippet(monaco);
editorDidMount && editorDidMount.call(this, arguments);
}}
/>
<a
onClick={this.fullScreen}
className="monaco_fullscreen_icon"
title={
isFullScreen ? `${this.i18n('cancelFullScreen')} cmd+shift+f` : `${this.i18n('fullScreen')} cmd+shift+f`
}
>
<Icon type={isFullScreen ? 'quxiaoquanping' : 'quanping'} />
</a>
</div>
</div>
);
}
//值变化
onChange(curValue) {
if (curValue === this.valuePrefix + this.strValue) return;
const { onAfterChange, language, autoSubmit, onChange } = this.props;
this.strValue = curValue; //记录当前格式
if (this.ct) clearTimeout(this.ct);
this.ct = setTimeout(() => {
this.position = this.editor.getPosition();
let ret = this.resultHandler(curValue, language);
if (autoSubmit) onChange && onChange(ret.value);
onAfterChange && onAfterChange(ret.value, ret.error, this.editor);
}, 300);
}
//提交动作
onSubmit() {
const { onSubmit, onChange, language } = this.props;
let curValue = this.editor.getValue();
let ret = this.resultHandler(curValue, language);
if (!ret.error) onChange && onChange(ret.value);
onSubmit && onSubmit(ret.value, ret.error, this.editor);
}
//值类型转换处理
valueHandler(value, language) {
let tarValue = value || '';
if (language === 'json') {
if (value && typeof value === 'object') {
tarValue = JSON.stringify(value, null, 2);
} else if (value && typeof value === 'string') {
try {
let ret = this.toJson(value);
if (!ret.error) tarValue = JSON.stringify(ret.value, null, 2);
} catch (err) {}
}
} else if (language === 'function') {
if (typeof value === 'function') {
tarValue = value.toString();
}
if (tarValue && typeof tarValue === 'string') {
tarValue = js_beautify(tarValue, { indent_size: 2, indent_empty_lines: true });
}
} else if (language === 'object') {
//先转成对象,在进行序列化和格式化;
value = value || {};
if (value && typeof value === 'object') {
try {
tarValue = serialize(value, { unsafe: true });
tarValue = js_beautify(tarValue, { indent_size: 2, indent_empty_lines: true });
} catch (err) {}
} else if (typeof value === 'string') {
try {
let ret = this.resultHandler(value, 'object');
tarValue = ret.error ? ret.value : serialize(ret.value, { unsafe: true });
tarValue = js_beautify(tarValue, { indent_size: 2, indent_empty_lines: true });
} catch (err) {}
}
}
return tarValue;
}
//结果处理
resultHandler(value, language) {
let ret = { value };
if (language === 'json') {
ret = this.toJson(value);
} else if (language === 'object') {
ret = this.toObject(value);
} else if (language === 'function') {
ret = this.toFunction(value);
}
return ret;
}
//设置全屏时的动作
fullScreen() {
if (!this.editorRef) return;
//还原到原来位置;
this.position = this.editor.getPosition();
if (this.state.isFullScreen) {
if (this.editorParentNode) {
if (this.editorParentNode.firstChild) {
this.editorParentNode.insertBefore(this.editorNode, this.editorParentNode.firstChild);
} else {
this.editorParentNode.appendChild(this.editorNode);
}
}
} else {
document.body.appendChild(this.editorNode);
}
let nextFs = !this.state.isFullScreen;
this.isFullScreenAction = true; //记录是全屏幕操作
this.setState(
{
isFullScreen: nextFs
},
() => {
this.editor.updateOptions(nextFs ? this.fullScreenOptions : this.options);
}
);
}
//美化代码
format() {
if (!this.editor) return;
if (/^\$_obj?\{.*?\}$/m.test(this.editor.getValue())) return;
if (this.props.language === 'json' || this.props.language === 'object' || this.props.language === 'function') {
let tarValue = js_beautify(this.editor.getValue(), { indent_size: 2 });
this.editor.setValue(tarValue);
} else if (this.props.language === 'less' || this.props.language === 'css' || this.props.language === 'scss') {
let tarValue = css_beautify(this.editor.getValue(), { indent_size: 2 });
this.editor.setValue(tarValue);
} else {
this.editor.getAction('editor.action.formatDocument').run();
}
}
//校验是否是json
toJson(value) {
try {
let obj = new Function(`'use strict'; return ${value.replace(/[\r\n\t]/g, '')}`)();
if (typeof obj === 'object' && obj) {
let tarValue = new Function(`'use strict'; return ${value}`)();
return { value: JSON.parse(JSON.stringify(tarValue)) };
}
return { error: this.i18n('jsonIllegal'), value };
} catch (err) {
return { error: err, value };
}
}
//校验是否为object对象
toObject(value) {
try {
let obj = new Function(`'use strict';return ${value}`)();
if (obj && typeof obj === 'object') {
if (jsonuri.isCircular(obj)) return { error: this.i18n('circularRef'), value };
return { value: obj };
} else {
return { error: this.i18n('objectIllegal'), value };
}
} catch (err) {
return { error: err, value };
}
}
//校验是否为function
toFunction(value) {
try {
let fun = new Function(`'use strict';return ${value}`)();
if (fun && typeof fun === 'function') {
return { value: fun };
} else {
return { error: this.i18n('functionIllegal'), value };
}
} catch (err) {
return { error: err, value };
}
}
//注册api和代码片段
registerApiAndSnippet(monaco) {
if (registerApiAndSnippetStatus) return;
registerApiAndSnippetStatus = true;
//注册this.提示的方法;
let thisSuggestions = [];
Snippets.map(item => {
if (!item.label || !item.kind || !item.insertText) return;
let tarItem = Object.assign(item, {
label: item.label,
kind: monaco.languages.CompletionItemKind[item.kind],
insertText: item.insertText
});
if (item.insertTextRules)
tarItem.insertTextRules = monaco.languages.CompletionItemInsertTextRule[item.insertTextRules];
thisSuggestions.push(tarItem);
});
monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: (model, position) => {
let textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
});
let match = textUntilPosition.match(/(^this\.)|(\sthis\.)/);
let suggestions = match ? thisSuggestions : [];
return { suggestions: suggestions };
},
triggerCharacters: ['.']
});
}
}
const prefix = 'data:text/javascript;charset=utf-8,';
const baseUrl = 'https://g.alicdn.com/iceluna/iceluna-vendor/0.0.1/';
window.MonacoEnvironment = {
getWorkerUrl: function(label: string) {
if (label === 'json') {
return `${prefix}${encodeURIComponent(`
importScripts('${baseUrl}json.worker.js');`)}`;
}
if (['css', 'less', 'scss'].includes(label)) {
return `${prefix}${encodeURIComponent(`
importScripts('${baseUrl}css.worker.js');`)}`;
}
if (label === 'html') {
return `${prefix}${encodeURIComponent(`
importScripts('${baseUrl}html.worker.js');`)}`;
}
if (['typescript', 'javascript'].includes(label)) {
return `${prefix}${encodeURIComponent(`
importScripts('${baseUrl}typescript.worker.js');`)}`;
}
return `${prefix}${encodeURIComponent(`
importScripts('${baseUrl}editor.worker.js');`)}`;
}
};
export default class MonacoEditorButtonView extends PureComponent {
static displayName = 'MonacoEditorButton';
static propTypes = {
locale: PropTypes.string,
messages: PropTypes.object
};
static defaultProps = {
locale: 'zh-CN',
messages: zhCN
};
i18n: any;
objectButtonRef: React.RefObject<unknown>;
constructor(props: Readonly<{}>) {
super(props);
this.i18n = generateI18n(props.locale, props.messages);
this.objectButtonRef = React.createRef();
// 兼容代码,待去除
window.__ctx.appHelper.constants = window.__ctx.appHelper.constants || {};
}
afterHandler(value: { nrs_temp_field: any; }) {
if (!value) return;
return value.nrs_temp_field;
}
beforeHandler(value: any) {
if (!value) return;
return { nrs_temp_field: value };
}
message(type: string, title: any, dom: Element | null) {
Message.show({
type,
title,
duration: 1000,
align: 'cc cc',
overlayProps: {
target: dom
}
});
}
componentDidMount() {
const { registerApi } = this.props;
let objectButtonThis = this.objectButtonRef;
registerApi &&
registerApi({
show: objectButtonThis.showModal,
hide: objectButtonThis.hideModal,
submit: objectButtonThis.submitHandler,
setValues: objectButtonThis.setValues
});
}
render() {
const self = this;
const { locale, messages, value, onChange, field, ...restProps } = this.props;
const { id } = field;
let tarRestProps = { ...restProps };
tarRestProps.autoSubmit = true;
tarRestProps.autoFocus = true;
let tarOnSubmit = tarRestProps.onSubmit;
//确保monaco快捷键保存能出发最外层的保存
tarRestProps.onSubmit = (value, error) => {
let msgDom = document.querySelector('.object-button-overlay .next-dialog-body');
if (error) return this.message('error', this.i18n('formatError'), msgDom);
this.objectButtonRef &&
this.objectButtonRef.current &&
this.objectButtonRef.current.submitHandler(() => {
this.message('success', this.i18n('saved'), msgDom);
});
};
let tarObjProps = { };
tarObjProps.className = 'luna-monaco-button';
if (tarRestProps['data-meta']) {
delete tarRestProps['data-meta'];
tarObjProps['data-meta'] = 'Field';
}
tarObjProps.id = id;
tarObjProps.value = value || '';
tarObjProps.onChange = onChange;
let tarRule = [];
//判断如果是jsonfunction, object等类型自动追加校验规则
if (tarRestProps.language && ['json', 'function', 'object'].includes(tarRestProps.language)) {
if (['json', 'object'].includes(tarRestProps.language)) {
tarRule.push({
validator: function(value: any, callback: (arg0: undefined) => void) {
if (typeof value !== 'object') {
callback(self.i18n('formatError'));
} else {
callback();
}
}
});
} else {
tarRule.push({
validator: function(value: any, callback: (arg0: undefined) => void) {
if (typeof value !== 'function') {
callback(self.i18n('formatError'));
} else {
callback();
}
}
});
}
}
return (
<div>
<ObjectButton
locale={locale}
messages={messages}
{...tarObjProps}
ref={this.objectButtonRef}
beforeHandler={this.beforeHandler.bind(this)}
afterHandler={this.afterHandler.bind(this)}
onSubmit={tarOnSubmit}
>
<FormItem name="nrs_temp_field" rules={tarRule}>
<MonacoEditorDefaultView {...tarRestProps} registerApi={(apis: any) => Object.assign(this, apis)} />
</FormItem>
</ObjectButton>
</div>
);
}
}

View File

@ -0,0 +1,242 @@
export default [
{
label: 'constants',
kind: 'Class',
insertText: 'constants',
detail: '应用全局常量',
documentation: '应用范围定义的通用常量'
},
{
label: 'utils',
kind: 'Class',
insertText: 'utils',
detail: '应用全局公共函数',
documentation: '应用范围扩展的公共函数'
},
{
label: 'state',
kind: 'Enum',
insertText: 'state',
detail: '当前所在容器组件内部状态',
documentation: 'React Class内部状态state'
},
{
label: 'setState',
kind: 'Function',
insertText: 'setState({\n\t$0\n})',
insertTextRules: 'InsertAsSnippet',
detail: '设置当前所在容器组件的state数据',
documentation: '原生React方法会自动更新组件视图'
},
{
label: 'reloadDataSource',
kind: 'Function',
insertText: 'reloadDataSource(${1:${2:namespace}, ${3:false}, ${4:callback}})',
insertTextRules: 'InsertAsSnippet',
detail: '刷新当前所在的容器组件',
documentation: '触发当前所在的容器组件,重新发送异步请求,并用最新数据更新视图'
},
{
label: 'location',
kind: 'Class',
insertText: 'location',
detail: '路由解析对象'
},
{
label: 'location.query',
kind: 'Value',
insertText: 'location.query.${1:xxxx}',
insertTextRules: 'InsertAsSnippet',
detail: '从路由解析对象中获取参数信息'
},
{
label: 'history',
kind: 'Class',
insertText: 'history',
detail: '路由历史对象'
},
{
label: 'React',
kind: 'Keyword',
insertText: 'React',
detail: 'React对象'
},
{
label: 'ReactDOM',
kind: 'Keyword',
insertText: 'ReactDOM',
detail: 'ReactDom对象'
},
{
label: 'ReactDOM.findDOMNode',
kind: 'Function',
insertText: 'ReactDOM.findDOMNode(${1:this.refs.xxxx})',
insertTextRules: 'InsertAsSnippet',
detail: 'ReactDom查找真实dom node'
},
{
label: 'Dialog.alert',
kind: 'Method',
insertText: [
'Dialog.alert({',
"\tcontent: '${1:Alert content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: 'alert弹框 By Fusion'
},
{
label: 'Dialog.confirm',
kind: 'Method',
insertText: [
'Dialog.confirm({',
"\tcontent: '${1:Confirm content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '确认弹出框 By Fusion'
},
{
label: 'Message.success',
kind: 'Method',
insertText: 'Message.success(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '成功反馈提示 By Fusion'
},
{
label: 'Message.error',
kind: 'Method',
insertText: 'Message.error(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '错误反馈提示 By Fusion'
},
{
label: 'Message.help',
kind: 'Method',
insertText: 'Message.help(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '帮助反馈提示 By Fusion'
},
{
label: 'Message.loading',
kind: 'Method',
insertText: 'Message.loading(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: 'loading反馈提示 By Fusion'
},
{
label: 'Message.notice',
kind: 'Method',
insertText: 'Message.notice(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '注意反馈提示 By Fusion'
},
{
label: 'Message.waining',
kind: 'Method',
insertText: 'Message.waining(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '警告反馈提示 By Fusion'
},
{
label: 'Modal.confirm',
kind: 'Method',
insertText: [
'Modal.confirm({',
"\tcontent: '${1:Confirm content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '确认弹出框 By Antd'
},
{
label: 'Modal.info',
kind: 'Method',
insertText: [
'Modal.info({',
"\tcontent: '${1:Info content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '信息弹出框 By Antd'
},
{
label: 'Modal.success',
kind: 'Method',
insertText: [
'Modal.success({',
"\tcontent: '${1:Success content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '成功弹出框 By Antd'
},
{
label: 'Modal.error',
kind: 'Method',
insertText: [
'Modal.error({',
"\tcontent: '${1:Error content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '错误弹出框 By Antd'
},
{
label: 'Modal.warning',
kind: 'Method',
insertText: [
'Modal.warning({',
"\tcontent: '${1:Warning content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '警告弹出框 By Antd'
}
];

View File

@ -0,0 +1,21 @@
import IntlMessageFormat from 'intl-messageformat';
export const isJSExpression = (obj = '') => {
if(obj && typeof obj === 'object' && obj.type === 'JSExpression') {
return true;
}
return false;
}
/**
* 用于构造国际化字符串处理函数
* @param {*} locale 国际化标识例如 zh-CNen-US
* @param {*} messages 国际化语言包
*/
export const generateI18n = (locale = 'zh-CN', messages = {}) => {
return function (key, values = {}) {
if (!messages || !messages[key]) return '';
const formater = new IntlMessageFormat(messages[key], locale);
return formater.format(values);
};
}

View File

@ -0,0 +1,36 @@
export default {
// function
setting: '点击设置',
edit: '编辑',
submitConfirm: '确认提交 cmd+s',
close: '关闭 esc',
fullScreen: '全屏',
cancelFullScreen: '取消全屏',
jsonIllegal: '非json格式',
functionIllegal: '非function格式',
objectIllegal: '非object格式',
circularRef: '对象中出现循环引用的对象',
formatError: '格式错误',
saved: '已保存',
// expression
valueIllegal: '值类型为对象类型,与当前组件属性设置的控件类型不匹配,请在属性“代码编辑模式”下进行编辑',
jsExpression: '请输入JS表达式',
// Mixin
input: '字符串Input',
textarea: '多行字符串Textarea',
expression: '变量控件Expression',
monacoEditor: '编辑器MonacoEditor',
numberPicker: '数字NumberPicker',
bool: '布尔Switch',
datePicker: '日期选择DatePicker',
select: '下拉选择Select',
radio: '单项选择RadioGroup',
date: '日期选择DatePicker',
dateYear: '年选择DatePicker',
dateMonth: '月选择DatePicker',
dateRange: '日期区间选择DatePicker',
list: '数组List',
object: '对象ObjectButton',
reactNode: '节点类型ReactNode',
typeError: 'Minix组件属性Types配置错误存在不支持类型[{type}],请检查组件属性配置',
};

View File

@ -0,0 +1,130 @@
// mixin
.lowcode-setter-mixin > * {
vertical-align: middle;
}
.lowcode-setter-mixin {
width: 86%;
}
.lowcode-setter-mixin .next-input {
width: 100%;
}
.lowcode-setter-mixin .next-select-trigger {
width: 100%;
}
// json-setter
// :global {
.nrs-monaco-form {
.next-form-item:last-child {
margin: 0 !important;
}
}
.monaco-editor-wrap {
.luna-monaco-button .next-icon-first {
height: 26px;
}
.monaco_fullscreen_icon {
position: absolute;
line-height: 1;
z-index: 7;
color: #ddd;
&:hover {
color: #fff;
}
}
.btns-eare {
text-align: left;
line-height: initial;
margin-top: 5px;
// button{
// margin-right: 10px;
// }
}
&.monaco-nofullscreen {
position: relative !important;
.monaco_fullscreen_icon {
position: absolute;
top: 5px;
right: 5px;
line-height: 1;
z-index: 7;
i:before {
font-size: 16px;
}
}
}
&.monaco-fullscreen {
position: fixed !important;
height: 100% !important;
width: 100% !important;
border: 0;
margin: 0;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1001;
overflow: hidden;
.monaco_fullscreen_icon {
top: 10px;
right: 10px;
i:before {
font-size: 24px;
}
}
}
}
.luna-monaco-button button {
width: 100%;
}
.luna-monaco-button-dialog {
.next-dialog-body {
padding: 0;
.next-form-item {
height: 100%;
margin-bottom: 0;
.next-form-item-control,
.next-form-item-control > div {
height: 100% !important;
}
.next-form-item-help {
position: absolute;
}
}
}
}
// }
// color-setter
.lowcode-color-box {
margin-right: -5px;
padding: 3px 0 3px 3px;
width: 26px;
height: 26px;
display: inline-block;
div {
width: 20px;
height: 20px;
border: 1px solid #ddd;
}
}
.next-balloon-normal.lowcode-color-content {
padding: 0;
background: #ffffff;
border-radius: 0;
border: 1px solid #e5e5e5;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
&:after {
display: none;
}
.sketch-picker {
border-radius: 0 !important;
border: none !important;
box-shadow: none !important;
.flexbox-fix {
input {
width: 100% !important;
min-width: 30px;
text-align: center;
}
}
}
}

View File

@ -0,0 +1,302 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Dropdown, Button, Menu, Icon, Input, NumberPicker, Switch, Select, Radio, DatePicker } from '@alifd/next';
import MonacoEditor from '@ali/iceluna-comp-monaco-editor';
import { isJSExpression, generateI18n } from './locale/utils';
import Expression from '../expression-setter';
import zhCN from './locale/zh-CN';
import './index.scss';
const { Group: RadioGroup } = Radio;
// const isJSExpression = (obj) => {
// if(typeof obj === 'object' && obj.type === 'JSExpression') {
// return true;
// }
// return false;
// }
export default class Mixin extends PureComponent {
static displayName = 'Mixin';
static propTypes = {
locale: PropTypes.string,
messages: PropTypes.object,
defaultType: PropTypes.string,
types: PropTypes.arrayOf(PropTypes.string),
onlyChangeType: PropTypes.bool,
inputProps: PropTypes.object,
expressionProps: PropTypes.object,
monacoEditorProps: PropTypes.object,
switchProps: PropTypes.object,
selectProps: PropTypes.object,
radioGroupProps: PropTypes.object,
};
static defaultProps = {
locale: 'zh-CN',
messages: zhCN,
types: ['StringSetter', 'ExpressionSetter', 'NumberSetter', 'BoolSetter', 'SelectSetter', 'RadioGroupSetter'],
};
constructor(props) {
super(props);
let type = judgeTypeHandler(props, {});
this.i18n = generateI18n(props.locale, props.messages);
this.state = {
preType: type,
type
};
}
static getDerivedStateFromProps(props, state) {
if ('value' in props) {
let curType = judgeTypeHandler(props, state);
if (curType !== state.preType) {
return {
type: curType
};
}
}
return null;
}
changeType(type) {
if (typeof type === 'object' || type === this.state.type) return;
let { onlyChangeType, value, onChange } = this.props;
if (onlyChangeType) {
this.setState({ type });
onChange && onChange(value);
} else {
let newValue = undefined;
if (this.typeMap[type]['props']) {
if (this.typeMap[type]['props']['value'] !== undefined) {
newValue = this.typeMap[type]['props']['value'];
} else if (this.typeMap[type]['props']['defaultValue'] !== undefined) {
newValue = this.typeMap[type]['props']['defaultValue'];
}
}
if (type === 'BoolSetter' && newValue === undefined) {
newValue = false; //给切换到switch默认值为false
}
this.setState({ type });
onChange && onChange(newValue);
}
}
render() {
const {
style = {},
className,
locale,
messages,
types = [],
defaultType,
// inputProps,
// expressionProps,
// monacoEditorProps,
// numberPickerProps,
// switchProps,
// selectProps,
// radioGroupProps,
...restProps
} = this.props;
this.typeMap = {
StringSetter: {
label: this.i18n('input'),
component: Input,
// props: inputProps
},
ExpressionSetter: {
label: this.i18n('expression'),
component: Expression,
// props: expressionProps
},
// MonacoEditor: {
// label: this.i18n('monacoEditor'),
// component: MonacoEditor,
// props: monacoEditorProps
// },
NumberSetter: {
label: this.i18n('numberPicker'),
component: NumberPicker,
},
BoolSetter: {
label: this.i18n('bool'),
component: Switch,
},
SelectSetter: {
label: this.i18n('select'),
component: Select,
},
RadioGroupSetter: {
label: this.i18n('radio'),
component: RadioGroup,
},
TextAreaSetter: {
label: this.i18n('textarea'),
component: Input.TextArea,
},
DateSetter: {
label: this.i18n('date'),
component: DatePicker,
},
DateYearSetter: {
label: this.i18n('dateYear'),
component: DatePicker,
},
DateMonthSetter: {
label: this.i18n('dateMonth'),
component: DatePicker,
},
DateRangeSetter: {
label: this.i18n('dateRange'),
component: DatePicker,
}
};
let realTypes = [];
types.forEach( el => {
const { name, props } = el;
if (this.typeMap[name]) {
this.typeMap[name].props = props;
realTypes.push(name);
}
})
let moreBtnNode = null;
//如果只有2种且有变量表达式则直接展示变量按钮
if (realTypes.length > 1) {
let isTwoType = !!(realTypes.length === 2 && ~realTypes.indexOf('ExpressionSetter'));
let btnProps = {
size: 'small',
text: true,
style: {
position: 'absolute',
left: '100%',
top: 0,
bottom: 0,
margin: 'auto 0 auto 8px',
padding: 0,
width: 16,
height: 16,
lineHeight: '16px',
textAlign: 'center'
}
};
if (isTwoType) {
btnProps.onClick = this.changeType.bind(this, realTypes.indexOf(this.state.type) ? realTypes[0] : realTypes[1]);
}
let triggerNode = (
<Button {...btnProps} size={isTwoType ? 'large' : 'small'}>
<Icon type={isTwoType ? 'edit' : 'ellipsis'} />
</Button>
);
if (isTwoType) {
moreBtnNode = triggerNode;
} else {
let MenuItems = [];
realTypes.map(type => {
if (this.typeMap[type]) {
MenuItems.push(<Menu.Item key={type}>{this.typeMap[type]['label']}</Menu.Item>);
} else {
console.error(
this.i18n('typeError', {
type
})
);
}
});
let MenuNode = (
<Menu
selectMode="single"
hasSelectedIcon={false}
selectedKeys={this.state.type}
onItemClick={this.changeType.bind(this)}
>
{MenuItems}
</Menu>
);
moreBtnNode = (
<Dropdown trigger={triggerNode} triggerType="click">
{MenuNode}
</Dropdown>
);
}
}
let TargetNode = this.typeMap[this.state.type] ? this.typeMap[this.state.type]['component'] : 'div';
let targetProps = this.typeMap[this.state.type] ? this.typeMap[this.state.type]['props'] : {};
// 特殊处理Switch的值
if (['BoolSetter', 'RadioGroupSetter'].includes(this.state.type)) {
restProps.checked = this.props.checked !== undefined ? this.props.checked : this.props.value;
}
//判断如果Mixin内部有设置onChange, 则同时触发2处onChange
if (targetProps && targetProps.onChange && typeof targetProps.onChange === 'function') {
let tarOnChange = targetProps.onChange;
targetProps.onChange = function() {
tarOnChange.apply(null, arguments);
restProps.onChange && restProps.onChange.apply(null, arguments);
};
}
let tarStyle = { position: 'relative', ...style };
let classes = classNames(className, 'lowcode-setter-mixin');
return (
<div style={tarStyle} className={classes}>
<TargetNode {...restProps} {...targetProps} />
{moreBtnNode}
</div>
);
}
}
// 判断值类型
function judgeTypeHandler(props, state) {
let { defaultType, types, value } = props;
let selectProps: { dataSource: any[]; };
let radioGroupProps: { dataSource: any[]; };
let typeKeys: any[] = [];
types.forEach( el => {
typeKeys.push(el.name);
})
types.forEach((el: { name: string; props: {}; }) => {
if (el.name === 'SelectSetter') {selectProps === el.props;}
if (el.name === 'RadioGroupSetter') {radioGroupProps === el.props;}
})
if (!defaultType || !typeKeys) return;
// 如果defaultType不在typeKeys列表中默认返回typeKeys的第一项
if (!typeKeys.includes(defaultType)) return typeKeys[0];
if (isJSExpression(value)) return 'ExpressionSetter';
if (value && typeof value === 'string') {
if (~typeKeys.indexOf('SelectSetter') && selectProps && selectProps.dataSource) {
let hasOption = selectProps.dataSource.some(item => {
if (typeof item === 'string' && item === value) return true;
if (typeof item === 'object' && item.value === value) return true;
});
if (hasOption) return 'SelectSetter';
}
if (~typeKeys.indexOf('RadioGroupSetter') && radioGroupProps && radioGroupProps.dataSource) {
let hasOption = radioGroupProps.dataSource.some(item => {
if (typeof item === 'object' && item.value === value) return true;
});
if (hasOption) return 'RadioGroupSetter';
}
if (~typeKeys.indexOf('StringSetter')) return 'StringSetter';
}
if (typeof value === 'number') {
if (~typeKeys.indexOf('SelectSetter') && selectProps && selectProps.dataSource) {
let hasOption = selectProps.dataSource.some(item => {
if (typeof item === 'object' && item.value === value) return true;
});
if (hasOption) return 'Select';
}
if (~typeKeys.indexOf('RadioGroupSetter') && radioGroupProps && radioGroupProps.dataSource) {
let hasOption = radioGroupProps.dataSource.some(item => {
if (typeof item === 'object' && item.value === value) return true;
});
if (hasOption) return 'RadioGroupSetter';
}
if (~typeKeys.indexOf('NumberSetter')) return 'NumberSetter';
}
if (~typeKeys.indexOf('NumberSetter') && typeof value === 'number') return 'NumberSetter';
if (~typeKeys.indexOf('BoolSetter') && (value === false || value === true)) return 'BoolSetter';
if (Array.isArray(value)) {
if (~typeKeys.indexOf('SelectSetter') && typeof value[0] === 'string') return 'SelectSetter';
}
return state.type || defaultType;
}

View File

@ -0,0 +1,242 @@
export default [
{
label: 'constants',
kind: 'Class',
insertText: 'constants',
detail: '应用全局常量',
documentation: '应用范围定义的通用常量'
},
{
label: 'utils',
kind: 'Class',
insertText: 'utils',
detail: '应用全局公共函数',
documentation: '应用范围扩展的公共函数'
},
{
label: 'state',
kind: 'Enum',
insertText: 'state',
detail: '当前所在容器组件内部状态',
documentation: 'React Class内部状态state'
},
{
label: 'setState',
kind: 'Function',
insertText: 'setState({\n\t$0\n})',
insertTextRules: 'InsertAsSnippet',
detail: '设置当前所在容器组件的state数据',
documentation: '原生React方法会自动更新组件视图'
},
{
label: 'reloadDataSource',
kind: 'Function',
insertText: 'reloadDataSource(${1:${2:namespace}, ${3:false}, ${4:callback}})',
insertTextRules: 'InsertAsSnippet',
detail: '刷新当前所在的容器组件',
documentation: '触发当前所在的容器组件,重新发送异步请求,并用最新数据更新视图'
},
{
label: 'location',
kind: 'Class',
insertText: 'location',
detail: '路由解析对象'
},
{
label: 'location.query',
kind: 'Value',
insertText: 'location.query.${1:xxxx}',
insertTextRules: 'InsertAsSnippet',
detail: '从路由解析对象中获取参数信息'
},
{
label: 'history',
kind: 'Class',
insertText: 'history',
detail: '路由历史对象'
},
{
label: 'React',
kind: 'Keyword',
insertText: 'React',
detail: 'React对象'
},
{
label: 'ReactDOM',
kind: 'Keyword',
insertText: 'ReactDOM',
detail: 'ReactDom对象'
},
{
label: 'ReactDOM.findDOMNode',
kind: 'Function',
insertText: 'ReactDOM.findDOMNode(${1:this.refs.xxxx})',
insertTextRules: 'InsertAsSnippet',
detail: 'ReactDom查找真实dom node'
},
{
label: 'Dialog.alert',
kind: 'Method',
insertText: [
'Dialog.alert({',
"\tcontent: '${1:Alert content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: 'alert弹框 By Fusion'
},
{
label: 'Dialog.confirm',
kind: 'Method',
insertText: [
'Dialog.confirm({',
"\tcontent: '${1:Confirm content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '确认弹出框 By Fusion'
},
{
label: 'Message.success',
kind: 'Method',
insertText: 'Message.success(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '成功反馈提示 By Fusion'
},
{
label: 'Message.error',
kind: 'Method',
insertText: 'Message.error(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '错误反馈提示 By Fusion'
},
{
label: 'Message.help',
kind: 'Method',
insertText: 'Message.help(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '帮助反馈提示 By Fusion'
},
{
label: 'Message.loading',
kind: 'Method',
insertText: 'Message.loading(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: 'loading反馈提示 By Fusion'
},
{
label: 'Message.notice',
kind: 'Method',
insertText: 'Message.notice(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '注意反馈提示 By Fusion'
},
{
label: 'Message.waining',
kind: 'Method',
insertText: 'Message.waining(${1:content})',
insertTextRules: 'InsertAsSnippet',
detail: '警告反馈提示 By Fusion'
},
{
label: 'Modal.confirm',
kind: 'Method',
insertText: [
'Modal.confirm({',
"\tcontent: '${1:Confirm content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '确认弹出框 By Antd'
},
{
label: 'Modal.info',
kind: 'Method',
insertText: [
'Modal.info({',
"\tcontent: '${1:Info content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '信息弹出框 By Antd'
},
{
label: 'Modal.success',
kind: 'Method',
insertText: [
'Modal.success({',
"\tcontent: '${1:Success content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '成功弹出框 By Antd'
},
{
label: 'Modal.error',
kind: 'Method',
insertText: [
'Modal.error({',
"\tcontent: '${1:Error content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '错误弹出框 By Antd'
},
{
label: 'Modal.warning',
kind: 'Method',
insertText: [
'Modal.warning({',
"\tcontent: '${1:Warning content}',",
"\ttitle: '${2:Title}',",
'\tonOk: () => {',
'\t\t$3',
'\t},',
'\tonCancel: () => {',
'\t\t$4',
'\t}',
'})'
].join('\n'),
insertTextRules: 'InsertAsSnippet',
detail: '警告弹出框 By Antd'
}
];

View File

@ -0,0 +1,21 @@
import IntlMessageFormat from 'intl-messageformat';
export const isJSExpression = (obj = '') => {
if(obj && typeof obj === 'object' && obj.type === 'JSExpression') {
return true;
}
return false;
}
/**
* 用于构造国际化字符串处理函数
* @param {*} locale 国际化标识例如 zh-CNen-US
* @param {*} messages 国际化语言包
*/
export const generateI18n = (locale = 'zh-CN', messages = {}) => {
return function (key, values = {}) {
if (!messages || !messages[key]) return '';
const formater = new IntlMessageFormat(messages[key], locale);
return formater.format(values);
};
}

View File

@ -0,0 +1,36 @@
export default {
// function
setting: '点击设置',
edit: '编辑',
submitConfirm: '确认提交 cmd+s',
close: '关闭 esc',
fullScreen: '全屏',
cancelFullScreen: '取消全屏',
jsonIllegal: '非json格式',
functionIllegal: '非function格式',
objectIllegal: '非object格式',
circularRef: '对象中出现循环引用的对象',
formatError: '格式错误',
saved: '已保存',
// expression
valueIllegal: '值类型为对象类型,与当前组件属性设置的控件类型不匹配,请在属性“代码编辑模式”下进行编辑',
jsExpression: '请输入JS表达式',
// Mixin
input: '字符串Input',
textarea: '多行字符串Textarea',
expression: '变量控件Expression',
monacoEditor: '编辑器MonacoEditor',
numberPicker: '数字NumberPicker',
bool: '布尔Switch',
datePicker: '日期选择DatePicker',
select: '下拉选择Select',
radio: '单项选择RadioGroup',
date: '日期选择DatePicker',
dateYear: '年选择DatePicker',
dateMonth: '月选择DatePicker',
dateRange: '日期区间选择DatePicker',
list: '数组List',
object: '对象ObjectButton',
reactNode: '节点类型ReactNode',
typeError: 'Minix组件属性Types配置错误存在不支持类型[{type}],请检查组件属性配置',
};