feat:「页面设计器」统一表达式组件 (#6552)

* 「页面设计器」统一表达式组件

* feat: 「页面设计器」统一表达式组件

---------

Co-authored-by: wutong25 <wutong25@baidu.com>
This commit is contained in:
igrowp 2023-04-07 17:06:08 +08:00 committed by GitHub
parent e42a68f360
commit d578e0a27e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1174 additions and 279 deletions

View File

@ -1,11 +1,27 @@
.ae-ExpressionFormulaControl {
background-color: #fff;
.btn-configured,
.btn-set-expression {
width: 100%;
font-size: 12px;
}
.btn-configured {
position: relative;
justify-content: left;
padding-right: 32px;
padding-left: 4px;
& > span {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
background-color: #007bff;
padding: 4px 8px;
border-radius: 4px;
color: #fff;
}
.icon {
position: absolute;
top: 0;

View File

@ -25,9 +25,9 @@ div.ae-editor-FormulaControl {
& &-input {
flex: 1;
border-radius: var(--Form-select-borderRadius) 0 0
var(--Form-select-borderRadius);
max-width: calc(100% - 35px); // 避免表达式内容太长撑开面板
border-radius: var(--input-default-default-top-left-border-radius) 0 0
var(--input-default-default-bottom-left-border-radius);
max-width: calc(100% - 29px); // 避免表达式内容太长撑开面板
&.is-clearable {
padding-right: 30px; // 避免间隙过大
@ -85,8 +85,8 @@ div.ae-editor-FormulaControl {
}
> div,
&:not(.border-wrapper) > div > div {
border-radius: var(--Form-select-borderRadius) 0 0
var(--Form-select-borderRadius);
border-radius: var(--input-default-default-top-left-border-radius) 0 0
var(--input-default-default-bottom-left-border-radius);
}
&.border-wrapper {
@ -97,21 +97,28 @@ div.ae-editor-FormulaControl {
&-tooltipBox {
flex: 1;
max-width: calc(100% - 35px); // 避免表达式内容太长撑开面板
max-width: calc(100% - 29px); // 避免表达式内容太长撑开面板
position: relative;
}
&-ResultBox-wrapper {
cursor: pointer;
}
& &-ResultBox {
flex-wrap: nowrap !important; // 不换行避免被其他样式覆盖
white-space: nowrap;
padding: 0 0 0 0.25rem;
border-radius: var(--Form-select-borderRadius) 0 0
var(--Form-select-borderRadius);
font-size: 12px;
border-radius: var(--input-default-default-top-left-border-radius) 0 0
var(--input-default-default-bottom-left-border-radius);
&.is-clearable {
& {
> div:first-child {
max-width: calc(100% - 30px); // 避免表达式内容太长撑开面板
background: #f7f7f9;
color: #151b26;
// background: var(--button-primary-default-bg-color);
background: #007bff;
color: #fff;
padding: 1px 5px;
border-radius: 4px 0 0 4px;
height: 26px;
@ -122,9 +129,11 @@ div.ae-editor-FormulaControl {
}
}
> div:last-child {
background: #f7f7f9;
// background: var(--button-primary-default-bg-color);
background: #007bff;
margin-right: 0.75rem;
border-radius: 0 4px 4px 0;
color: #fff;
height: 26px;
svg {
fill: var(--Form-input-clearBtn-color);
@ -136,12 +145,7 @@ div.ae-editor-FormulaControl {
}
span.c-field {
background: #007bff;
padding: 3px 5px;
margin: 0 1px;
color: #fff;
font-size: 12px;
border-radius: 4px;
}
span.c-func {
@ -152,9 +156,24 @@ div.ae-editor-FormulaControl {
}
}
.input-clear-icon {
position: absolute;
top: 0;
bottom: 0;
right: 8px;
margin: auto;
height: 14px;
fill: var(--Form-input-clearBtn-color);
cursor: pointer;
&:hover {
fill: var(--Form-input-clearBtn-color-onHover);
}
}
&-icon {
top: 0 !important;
font-size: #{px2rem(18px)};
font-size: #{px2rem(14px)};
&:not(:last-child) {
margin-right: var(--fontSizeSm);

View File

@ -15,6 +15,7 @@
border-radius: var(--Form-input-borderRadius);
background: var(--Form-input-bg);
font-size: var(--Form-input-fontSize);
padding-bottom: 26px;
&::placeholder {
color: var(--Form-input-placeholderColor);
}
@ -27,15 +28,11 @@
height: 100%;
border-radius: var(--Form-input-borderRadius);
overflow: auto;
padding-left: 8px;
& > .CodeMirror {
height: 100%;
padding-bottom: 26px;
.CodeMirror-lines {
padding-bottom: 30px;
}
.CodeMirror-vscrollbar {
margin-bottom: 26px;
}
font-family: inherit;
// 解决上下 pre标签中表达式浮层遮挡问题
.CodeMirror-measure + div {
@ -46,78 +43,16 @@
z-index: initial;
}
}
}
.cm-expression {
position: relative;
display: inline-block;
.expression-popover {
position: absolute;
display: none;
left: 0;
bottom: -30px;
transform: translate(calc(24px - 50%));
font-size: 12px;
width: max-content;
color: #fff;
border-radius: 4px;
padding: 2px 8px;
background: var(--Tooltip-bg--dark);
border: none;
box-shadow: var(--Tooltip-boxShadow--dark);
z-index: 10;
.expression-popover-arrow {
position: absolute;
display: block;
width: var(--Tooltip-arrow-width);
height: var(--Tooltip-arrow-height);
margin-left: calc(var(--Tooltip-arrow-width) * -1 / 2);
left: 50%;
top: calc(var(--Tooltip-arrow-height) * -1);
&::before,
&::after {
position: absolute;
display: block;
content: '';
border-color: transparent;
border-style: solid;
border-width: 0 calc(var(--Tooltip-arrow-width) / 2)
var(--Tooltip-arrow-height) calc(var(--Tooltip-arrow-width) / 2);
}
&::before {
border-width: 0;
}
&::after {
border-bottom-color: var(--Tooltip-arrow-color--dark);
}
}
}
}
.cm-expression-text {
display: inline-block;
position: relative;
padding: 0 5px;
border-radius: 3px;
color: #fff;
margin: 0 1px 1px;
background: var(--button-primary-default-bg-color);
cursor: pointer;
width: 40px;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
&:hover {
background: var(--button-primary-hover-bg-color);
& ~ .expression-popover {
display: block;
}
}
}
&-placeholder {
position: absolute;
line-height: 28px;
top: 0;
left: 13px;
font-size: 12px;
color: var(--text--muted-color);
pointer-events: none;
}
&-footer {
@ -143,7 +78,7 @@
}
&-fxIcon {
> a {
font-size: 19px;
font-size: 18px;
}
}
}
@ -199,3 +134,83 @@
}
}
}
.cm-expression {
position: relative;
display: inline-block;
border-radius: 4px;
padding: 0 5px;
margin: 0 1px 1px;
background: var(--button-primary-default-bg-color);
&-text {
display: inline-block;
position: relative;
color: #fff;
cursor: pointer;
max-width: 80px;
min-width: 10px;
min-height: 18px;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
&:hover {
background: var(--button-primary-hover-bg-color);
& ~ .cm-expression-popover {
display: block;
}
}
}
&-close {
font-size: 12px;
color: white;
cursor: pointer;
}
&-popover {
position: absolute;
display: none;
left: 0;
bottom: -30px;
transform: translate(calc(24px - 50%));
font-size: 12px;
width: max-content;
color: #fff;
border-radius: 4px;
padding: 2px 8px;
background: var(--Tooltip-bg--dark);
border: none;
box-shadow: var(--Tooltip-boxShadow--dark);
z-index: 10;
&-arrow {
position: absolute;
display: block;
width: var(--Tooltip-arrow-width);
height: var(--Tooltip-arrow-height);
margin-left: calc(var(--Tooltip-arrow-width) * -1 / 2);
left: 50%;
top: calc(var(--Tooltip-arrow-height) * -1);
&::before,
&::after {
position: absolute;
display: block;
content: '';
border-color: transparent;
border-style: solid;
border-width: 0 calc(var(--Tooltip-arrow-width) / 2)
var(--Tooltip-arrow-height) calc(var(--Tooltip-arrow-width) / 2);
}
&::before {
border-width: 0;
}
&::after {
border-bottom-color: var(--Tooltip-arrow-color--dark);
}
}
}
}

View File

@ -0,0 +1,133 @@
.ae-TplFormulaControl {
position: relative;
width: 100%;
display: flex;
height: 32px;
.ae-TplResultBox {
position: relative;
flex: 1;
overflow-x: auto;
border: 1px solid var(--Form-input-borderColor);
border-radius: var(--input-default-default-top-left-border-radius) 0 0
var(--input-default-default-bottom-left-border-radius);
background: var(--Form-input-bg);
font-size: var(--Form-input-fontSize);
padding: 0 8px;
.input-clear-icon {
position: absolute;
top: 0;
bottom: 0;
right: 8px;
margin: auto;
height: 14px;
fill: var(--Form-input-clearBtn-color);
cursor: pointer;
&:hover {
fill: var(--Form-input-clearBtn-color-onHover);
}
}
&::placeholder {
color: var(--Form-input-placeholderColor);
}
&:hover {
border-color: var(--Form-input-onHover-borderColor);
}
&-editor {
border-radius: var(--Form-input-borderRadius);
& > .CodeMirror {
height: 100%;
color: var(--Form-input-color);
font-family: inherit;
// 解决上下 pre标签中表达式浮层遮挡问题
.CodeMirror-measure + div {
z-index: inherit !important;
}
pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
z-index: initial;
}
.CodeMirror-sizer {
min-height: 30px !important;
}
.CodeMirror-hscrollbar {
display: none !important;
}
.CodeMirror-lines {
line-height: 20px;
}
}
}
.cm-expression {
background: #007bff;
padding: 0px 5px;
margin: 0 1px;
color: #fff;
font-size: 12px;
border-radius: 4px;
}
}
&-button {
height: auto;
background-color: #f7f7f9;
padding: 4px 8px;
border-radius: 0 var(--input-default-default-top-left-border-radius)
var(--input-default-default-bottom-left-border-radius) 0;
border-left: 0;
&:not(:disabled):not(.is-disabled) {
&:hover,
&:hover:active {
border-color: #e6f0ff;
background-color: #e6f0ff;
border-left-width: 0;
}
}
}
&-icon {
font-size: #{px2rem(14px)};
&:not(:last-child) {
margin-right: var(--fontSizeSm);
}
&.is-filled {
fill: var(--primary);
color: var(--primary);
}
}
&-placeholder {
position: absolute;
line-height: 30px;
top: 0;
left: 14px;
font-size: 12px;
color: var(--text--muted-color);
pointer-events: none;
}
&-tooltip {
position: absolute;
top: 6px;
height: 18px;
z-index: 1;
cursor: pointer;
}
&.clearable {
.ae-TplResultBox {
padding-right: 28px;
}
}
}

View File

@ -32,6 +32,7 @@
@import './control/feature-control';
@import './control/databinding-control';
@import './control/event-action';
@import './control/tpl-formula-control';
@import './control/timeline_item_control';
@import './control/tree_option_control';
@import './control/_inpupt-file';

View File

@ -153,6 +153,10 @@ import layout_3_2 from './layout/layout3-2.svg';
import layout_free_container from './layout/layout-free-container.svg';
import layout_fixed_top from './layout/layout-fixed-top.svg';
// 其他类 icon
import inputAddFx from './other/+fx.svg';
import inputFx from './other/fx.svg';
// 属性配置面板/显示类型
import block from './display/block.svg';
import inline from './display/inline.svg';
@ -314,6 +318,9 @@ registerIcon('layout-3-2-plugin', layout_3_2);
registerIcon('layout-free-container', layout_free_container);
registerIcon('layout-fixed-top', layout_fixed_top);
registerIcon('input-add-fx', inputAddFx);
registerIcon('input-fx', inputFx);
// 属性配置面板/显示类型
registerIcon('inline-display', inline);
registerIcon('inline-block-display', inline_block);

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<rect x="0" y="0" width="16" height="16"></rect>
<path d="M9.89726996,1.42896935 C10.7622099,0.875052946 11.5432726,0.94041116 12.2245758,1.01677633 C12.5114403,1.04904331 13.1427595,1.10712387 13.0775631,1.72880092 C13.0264925,2.22571234 12.2929786,2.16645279 12.0985292,2.17516075 C10.969708,2.22571234 10.2688899,1.60213545 9.57889382,4.10820386 L9.01001898,6.66018234 L9.89726996,6.65262698 C10.1971738,6.65262698 10.440574,6.89355373 10.440574,7.1904099 C10.440574,7.48726607 10.1971738,7.72819282 9.89726996,7.72819282 L8.77857147,7.73574818 L7.23361155,13.8963048 C7.23361155,13.8963048 7.09235251,15.042858 6.38062427,14.9987598 C6.35250991,14.9970159 6.32580901,14.9939663 6.30045749,14.9897032 L6.36964031,14.9691045 C6.31633924,14.9895282 6.2597435,15 6.20266347,15 L4.96959782,15 C4.71186493,15 4.50293115,14.7910662 4.50293115,14.5333333 L4.50293115,14.5005208 C4.5022854,14.259116 4.6858559,14.0570646 4.92622202,14.0346853 C5.29023392,14.0001846 5.620688,13.9495418 5.91758425,13.8827569 C5.92851129,13.830951 5.93837482,13.8005794 5.93837482,13.8005794 L7.47138205,7.73574818 L6.2633946,7.73574818 C5.97978992,7.73574818 5.75377545,7.51848389 5.72987008,7.2442146 C5.75377545,6.74084978 5.97978992,6.66018234 6.2633946,6.66018234 L7.70500277,6.66018234 L8.29126334,4.07593689 C8.53249032,2.94013936 9.03015678,1.98503689 9.89726996,1.42896935 Z M14.7815274,5.87796146 C14.9187471,5.96723342 14.9900301,6.07048774 14.9989404,6.18664885 C15.0069598,6.30280996 14.9695362,6.41681994 14.8839967,6.52975435 L13.2738929,8.6378634 L14.8626118,11.2676219 C14.9125099,11.3461382 14.9249844,11.440788 14.8964712,11.5440423 C14.8715222,11.6494478 14.7592515,11.7225862 14.6104483,11.8193872 C14.4491706,11.9215659 14.3092778,11.9118858 14.2094817,11.8785433 C14.1123587,11.8441252 14.0250371,11.7666844 13.951972,11.6494478 L12.6118524,9.50907173 L10.98571,11.6386921 C10.7914639,11.8925256 10.5713778,11.9301704 10.3201054,11.7505509 C10.2167451,11.6774125 10.0278453,11.4063699 10.0055693,11.2837554 C9.98418444,11.1622164 10.0242811,11.0342241 10.1276414,10.8976272 L11.925754,8.53568465 L10.6159296,6.44370909 C10.5615764,6.34798373 10.5562302,6.23612488 10.5989999,6.10920811 C10.6399876,5.98229134 10.7371107,5.90700173 10.840471,5.83278769 C10.9892741,5.7316845 11.1256028,5.7284578 11.22629,5.7381379 C11.3278682,5.74566686 11.419645,5.81557864 11.5042935,5.94464654 L12.5931407,7.65802292 L13.9109843,5.92528635 C14.0250371,5.772556 14.1542374,5.71877771 14.2789826,5.67575508 C14.4028367,5.63273244 14.6478719,5.78761392 14.7815274,5.87796146 Z" fill="currentColor" fill-rule="nonzero"></path>
<rect fill="currentColor" x="0.897847116" y="6.66018234" width="4.5" height="1" rx="0.5"></rect>
<rect fill="currentColor" transform="translate(3.147847, 7.160182) rotate(-270.000000) translate(-3.147847, -7.160182) " x="0.897847116" y="6.66018234" width="4.5" height="1" rx="0.5"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<rect x="0" y="0" width="16" height="16"></rect>
<path d="M6.85458095,1.4369501 C7.71952092,0.883033689 9.14623722,0.984136878 9.82754043,1.06050205 C10.1144049,1.09276903 10.7457242,1.15084958 10.6805277,1.77252664 C10.6294571,2.26943806 9.89599674,2.21888646 9.7014939,2.21888646 C9.48535769,2.21888646 9.2659886,2.19876183 9.04755999,2.18089337 L8.82959943,2.1646059 C7.92402473,2.10678268 7.05892911,2.21764793 6.53620481,4.11618461 L6.21348223,5.68866187 L7.84556744,5.68866187 C8.14547125,5.68866187 8.38887144,5.92958862 8.38887144,6.22644479 C8.38887144,6.52330096 8.14547125,6.76422771 7.84556744,6.76422771 L5.98203473,6.76422771 L4.51511393,13.8963048 C4.51511393,13.8963048 4.37385489,15.042858 3.66212666,14.9987598 C3.63797714,14.9972618 3.61487053,14.9948005 3.5927662,14.9914342 C3.56428372,14.9965963 3.53191413,15 3.49937138,15 L1.46666667,15 C1.20893378,15 1,14.7910662 1,14.5333333 L1,14.5005208 C0.999354255,14.259116 1.18292475,14.0570646 1.42329087,14.0346853 C1.93951851,13.9857578 2.38825526,13.9043655 2.76950112,13.7905085 C2.88970413,13.7546105 2.99265144,13.7126424 3.07834304,13.6646043 C3.13612492,13.6324776 3.19778289,13.6139204 3.25964158,13.6078865 L4.67484531,6.76422771 L2.46685786,6.76422771 C2.18325317,6.76422771 1.95723871,6.54696341 1.93333333,6.27269412 L1.945,6.342 L1.95324336,6.32324571 C1.96746558,6.3067537 1.99022114,6.29312987 1.99875447,6.27376968 C1.98595447,6.2576362 1.94115447,6.24365384 1.94115447,6.22644479 L1.941,6.319 L1.93333333,6.27269412 C1.95524659,5.81127637 2.14698763,5.70504184 2.39724012,5.69054037 L2.46685786,5.68866187 L4.90846603,5.68866187 L5.24857433,4.08391763 C5.48980131,2.9481201 5.98746777,1.99301764 6.85458095,1.4369501 Z M14.7335755,5.87796146 C14.9009132,5.96723342 14.9878418,6.07048774 14.9987079,6.18664885 C15.0084874,6.30280996 14.9628498,6.41681994 14.8585355,6.52975435 L12.8950348,8.6378634 L14.8324569,11.2676219 C14.8933069,11.3461382 14.9085194,11.440788 14.873748,11.5440423 C14.8433229,11.6494478 14.7064103,11.7225862 14.5249468,11.8193872 C14.3282708,11.9215659 14.1576733,11.9118858 14.0359732,11.8785433 C13.9175329,11.8441252 13.8110453,11.7666844 13.7219435,11.6494478 L12.0876851,9.50907173 L10.1046255,11.6386921 C9.86774493,11.8925256 9.59935275,11.9301704 9.2929293,11.7505509 C9.16688277,11.6774125 8.93652187,11.4063699 8.90935667,11.2837554 C8.88327808,11.1622164 8.93217544,11.0342241 9.05822197,10.8976272 L11.2509969,8.53568465 L9.65368315,6.44370909 C9.58740006,6.34798373 9.58088042,6.23612488 9.6330376,6.10920811 C9.68302157,5.98229134 9.80146184,5.90700173 9.92750837,5.83278769 C10.1089719,5.7316845 10.2752229,5.7284578 10.3980096,5.7381379 C10.5218829,5.74566686 10.6338036,5.81557864 10.7370313,5.94464654 L12.0648663,7.65802292 L13.6719595,5.92528635 C13.8110453,5.772556 13.9686035,5.71877771 14.1207286,5.67575508 C14.2717671,5.63273244 14.5705843,5.78761392 14.7335755,5.87796146 Z" fill="currentColor" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -161,6 +161,7 @@ import './renderer/StatusControl';
import './renderer/FormulaControl';
import './renderer/ExpressionFormulaControl';
import './renderer/textarea-formula/TextareaFormulaControl';
import './renderer/TplFormulaControl';
import './renderer/DateShortCutControl';
import './renderer/BadgeControl';
import './renderer/style-control/BoxModel';

View File

@ -239,8 +239,9 @@ export class TextControlPlugin extends BasePlugin {
form.changeValue('validationErrors', {...validationErrors});
}
}),
getSchemaTpl('valueFormula', {
rendererSchema: context?.schema
getSchemaTpl('tplFormulaControl', {
name: 'value',
label: '默认值'
}),
getSchemaTpl('clearable'),
getSchemaTpl('showCounter', {

View File

@ -6,9 +6,10 @@ import React from 'react';
import {autobind, FormControlProps} from 'amis-core';
import cx from 'classnames';
import {FormItem, Button, Icon, PickerContainer} from 'amis';
import {FormulaEditor} from 'amis-ui/lib/components/formula/Editor';
import type {VariableItem} from 'amis-ui/lib/components/formula/Editor';
import {FormulaEditor} from 'amis-ui';
import type {VariableItem} from 'amis-ui';
import {getVariables} from './textarea-formula/utils';
import {renderFormulaValue} from './FormulaControl';
import {reaction} from 'mobx';
interface ExpressionFormulaControlProps extends FormControlProps {
@ -115,13 +116,6 @@ export default class ExpressionFormulaControl extends React.Component<
});
}
@autobind
renderFormulaValue(item: any) {
const html = {__html: item.html};
// bca-disable-next-line
return <span dangerouslySetInnerHTML={html}></span>;
}
@autobind
handleConfirm(value = '') {
if (this.props.evalMode) {
@ -190,11 +184,11 @@ export default class ExpressionFormulaControl extends React.Component<
mouseLeaveDelay: 20,
content: value,
tooltipClassName: 'btn-configured-tooltip',
children: () => this.renderFormulaValue(highlightValue)
children: () => renderFormulaValue(highlightValue)
}}
onClick={onClick}
>
{renderFormulaValue(highlightValue)}
<Icon
icon="input-clear"
className="icon"

View File

@ -23,9 +23,11 @@ import {
TooltipWrapper
} from 'amis';
import {FormulaExec, isExpression} from 'amis';
import {PickerContainer, relativeValueRe} from 'amis';
import {FormulaEditor} from 'amis-ui/lib/components/formula/Editor';
import {FormulaEditor} from 'amis-ui';
import FormulaPicker, {
CustomFormulaPickerProps
} from './textarea-formula/FormulaPicker';
import {autobind, translateSchema} from 'amis-editor-core';
import type {
@ -44,6 +46,12 @@ export enum FormulaDateType {
IsRange // 日期时间范围类
}
export function renderFormulaValue(item: any) {
const html = {__html: typeof item === 'string' ? item : item.html};
// bca-disable-next-line
return <span dangerouslySetInnerHTML={html}></span>;
}
export interface FormulaControlProps extends FormControlProps {
manager?: EditorManager;
@ -112,7 +120,7 @@ export interface FormulaControlProps extends FormControlProps {
* 备注2: 开关组件可以设置 true false boolean
* 备注3: 默认都是字符串类型
*/
valueType?: string;
// valueType?: string;
/**
* props
@ -131,6 +139,11 @@ export interface FormulaControlProps extends FormControlProps {
* FormulaDateType.NotDate
*/
DateTimeType?: FormulaDateType;
/**
* fx面板
*/
customFormulaPicker?: React.FC<CustomFormulaPickerProps>;
}
interface FormulaControlState {
@ -138,6 +151,8 @@ interface FormulaControlState {
variables: any;
variableMode?: 'tree' | 'tabs';
formulaPickerOpen: boolean;
}
export default class FormulaControl extends React.Component<
@ -159,7 +174,8 @@ export default class FormulaControl extends React.Component<
super(props);
this.state = {
variables: [],
variableMode: 'tabs'
variableMode: 'tabs',
formulaPickerOpen: false
};
}
@ -353,6 +369,20 @@ export default class FormulaControl extends React.Component<
? value
: `\${${value}}`;
this.props?.onChange?.(val);
this.closeFormulaPicker();
}
@autobind
handleFormulaClick() {
this.setState({
formulaPickerOpen: true
});
}
@autobind
closeFormulaPicker() {
this.setState({formulaPickerOpen: false});
}
handleSimpleInputChange = (value: any) => {
@ -452,13 +482,6 @@ export default class FormulaControl extends React.Component<
return curRendererSchema;
}
@autobind
renderFormulaValue(item: any) {
const html = {__html: item.html};
// bca-disable-next-line
return <span dangerouslySetInnerHTML={html}></span>;
}
@autobind
getContextData() {
// 当前数据域
@ -475,16 +498,18 @@ export default class FormulaControl extends React.Component<
label,
value,
header,
variables,
placeholder,
simple,
rendererSchema,
rendererWrapper,
manager,
useExternalFormData = false,
customFormulaPicker,
clearable = true,
render,
...rest
} = this.props;
const {formulaPickerOpen, variables, variableMode} = this.state;
// 自身字段
const selfName = this.props?.data?.name;
@ -501,13 +526,15 @@ export default class FormulaControl extends React.Component<
}
// 判断是否含有公式表达式
const isTypeError = !this.isExpectType(value);
// const isTypeError = !this.isExpectType(value);
const exprValue = this.transExpr(value);
const isError = isLoop || isTypeError;
const isError = isLoop;
const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker;
const highlightValue = isExpression(value)
? FormulaEditor.highlightValue(exprValue, this.state.variables) || {
? FormulaEditor.highlightValue(exprValue, variables) || {
html: exprValue
}
: value;
@ -577,85 +604,82 @@ export default class FormulaControl extends React.Component<
tooltipTheme: 'dark',
mouseLeaveDelay: 20,
content: exprValue,
children: () => this.renderFormulaValue(highlightValue)
children: () => renderFormulaValue(highlightValue)
}}
>
<div className="ae-editor-FormulaControl-tooltipBox">
<ResultBox
className={cx(
'ae-editor-FormulaControl-ResultBox',
isError ? 'is-error' : ''
)}
allowInput={false}
clearable={true}
value={value}
result={{
html: this.hasDateShortcutkey(value)
? '已配置相对值'
: '已配置表达式'
}}
itemRender={this.renderFormulaValue}
onChange={this.handleInputChange}
onResultChange={() => {
this.handleInputChange(undefined);
}}
/>
<div
className="ae-editor-FormulaControl-ResultBox-wrapper"
onClick={this.handleFormulaClick}
>
<ResultBox
className={cx(
'ae-editor-FormulaControl-ResultBox',
isError ? 'is-error' : ''
)}
allowInput={false}
value={value}
result={{
html: this.hasDateShortcutkey(value)
? value
: highlightValue.html
}}
itemRender={renderFormulaValue}
onChange={this.handleInputChange}
onResultChange={() => {
this.handleInputChange(undefined);
}}
/>
</div>
{value && (
<Icon
icon="input-clear"
className="input-clear-icon"
iconContent="InputText-clear"
onClick={() => this.handleInputChange('')}
/>
)}
</div>
</TooltipWrapper>
)}
<PickerContainer
showTitle={false}
bodyRender={({
value,
onChange
}: {
onChange: (value: any) => void;
value: any;
}) => {
return (
<FormulaEditor
{...rest}
evalMode={true}
variableMode={rest.variableMode ?? this.state.variableMode}
variables={this.state.variables}
header={header || '表达式'}
value={filterValue}
onChange={onChange}
selfVariableName={selfName}
/>
);
<Button
className="ae-editor-FormulaControl-button"
size="sm"
tooltip={{
enterable: false,
content: '点击配置表达式',
tooltipTheme: 'dark',
placement: 'left',
mouseLeaveDelay: 0
}}
value={value}
onConfirm={this.handleConfirm}
size="md"
onClick={this.handleFormulaClick}
>
{({onClick}: {onClick: (e: React.MouseEvent) => void}) => (
<Button
className="ae-editor-FormulaControl-button"
size="sm"
tooltip={{
enterable: false,
content: '点击配置表达式',
placement: 'left',
mouseLeaveDelay: 0
}}
onClick={onClick}
// active={simple && value} // 不需要,避免 hover 时无任何反馈效果
>
<Icon
icon="function"
className={cx('ae-editor-FormulaControl-icon', 'icon', {
['is-filled']: !!isFx
})}
/>
</Button>
)}
</PickerContainer>
<Icon
icon="input-fx"
className={cx('ae-editor-FormulaControl-icon', 'icon', {
['is-filled']: !!isFx
})}
/>
</Button>
{isError && (
<div className="desc-msg error-msg">
{isLoop ? '当前表达式异常(存在循环引用)' : '数值类型不匹配'}
</div>
)}
{formulaPickerOpen ? (
<FormulaPickerCmp
{...this.props}
value={filterValue}
initable={true}
header={header}
variables={variables}
variableMode={rest.variableMode ?? variableMode}
evalMode={true}
onClose={this.closeFormulaPicker}
onConfirm={this.handleConfirm}
/>
) : null}
</div>
);
}

View File

@ -0,0 +1,452 @@
/**
* @file
*/
import React from 'react';
import cx from 'classnames';
import {reaction} from 'mobx';
import {CodeMirrorEditor, FormulaEditor} from 'amis-ui';
import type {VariableItem, CodeMirror} from 'amis-ui';
import {Icon, Button, FormItem, TooltipWrapper} from 'amis';
import {autobind, FormControlProps} from 'amis-core';
import {FormulaPlugin, editorFactory} from './textarea-formula/plugin';
import {getVariables} from './textarea-formula/utils';
import {renderFormulaValue} from './FormulaControl';
import FormulaPicker, {
CustomFormulaPickerProps
} from './textarea-formula/FormulaPicker';
export interface TplFormulaControlProps extends FormControlProps {
/**
*
*/
variables?: Array<VariableItem> | Function;
/**
* variables 使
* props.variables amis数据域中取变量集合 false;
*/
requiredDataPropsVariables?: boolean;
/**
* 'tabs' 'tree'
*/
variableMode?: 'tree' | 'tabs';
/**
* fx面板
*/
customFormulaPicker?: React.FC<CustomFormulaPickerProps>;
/**
*
*/
clearable?: boolean;
/**
* "表达式"
*/
header: string;
}
interface TplFormulaControlState {
value: string; // 当前文本值
variables: Array<VariableItem>; // 变量数据
formulaPickerOpen: boolean; // 是否打开公式编辑器
formulaPickerValue: string; // 公式编辑器内容
expressionBrace?: Array<CodeMirror.Position>; // 表达式所在位置
tooltipStyle: {[key: string]: string}; // 提示框样式
}
// 暂时记录输入的字符,用于快捷键判断
let preInputLocation: {start: number; end: number} | null = {
start: 0,
end: 0
};
export class TplFormulaControl extends React.Component<
TplFormulaControlProps,
TplFormulaControlState
> {
static defaultProps: Partial<TplFormulaControlProps> = {
variableMode: 'tabs',
requiredDataPropsVariables: false,
placeholder: '请输入'
};
wrapRef = React.createRef<HTMLDivElement>();
tooltipRef = React.createRef<HTMLDivElement>();
editorPlugin: FormulaPlugin;
unReaction: any;
appLocale: string;
appCorpusData: any;
constructor(props: TplFormulaControlProps) {
super(props);
this.state = {
value: '',
variables: [],
formulaPickerOpen: false,
formulaPickerValue: '',
tooltipStyle: {
display: 'none'
}
};
}
async componentDidMount() {
const editorStore = (window as any).editorStore;
this.appLocale = editorStore?.appLocale;
this.appCorpusData = editorStore?.appCorpusData;
this.unReaction = reaction(
() => editorStore?.appLocaleState,
async () => {
this.appLocale = editorStore?.appLocale;
this.appCorpusData = editorStore?.appCorpusData;
const variablesArr = await getVariables(this);
this.setState({
variables: variablesArr
});
}
);
const variablesArr = await getVariables(this);
this.setState({
variables: variablesArr
});
if (this.tooltipRef.current) {
this.tooltipRef.current.addEventListener(
'mouseleave',
this.hiddenToolTip
);
}
if (this.wrapRef.current) {
this.wrapRef.current.addEventListener(
'keydown',
this.handleKeyDown,
true
);
}
}
async componentDidUpdate(prevProps: TplFormulaControlProps) {
if (this.props.data !== prevProps.data) {
const variablesArr = await getVariables(this);
this.setState({
variables: variablesArr
});
}
}
componentWillUnmount() {
if (this.tooltipRef.current) {
this.tooltipRef.current.removeEventListener(
'mouseleave',
this.hiddenToolTip
);
}
if (this.wrapRef.current) {
this.wrapRef.current.removeEventListener('keydown', this.handleKeyDown);
}
this.editorPlugin?.dispose();
this.unReaction?.();
}
@autobind
onExpressionClick(expression: string, brace?: Array<CodeMirror.Position>) {
this.setState({
formulaPickerValue: expression,
formulaPickerOpen: true,
expressionBrace: brace
});
}
@autobind
onExpressionMouseEnter(
e: MouseEvent,
expression: string,
brace?: Array<CodeMirror.Position>
) {
const wrapperRect = this.wrapRef.current?.getBoundingClientRect();
const expressionRect = (
e.target as HTMLSpanElement
).getBoundingClientRect();
if (!wrapperRect) {
return;
}
const left = expressionRect.left - wrapperRect.left;
this.setState({
tooltipStyle: {
left: `${left}px`,
width: `${expressionRect.width}px`
},
formulaPickerValue: expression,
expressionBrace: brace
});
}
@autobind
hiddenToolTip() {
this.setState({
tooltipStyle: {
display: 'none'
}
});
}
@autobind
handleKeyDown(e: any) {
// 组件禁止回车折行,否则会导致内容超过一行
if (e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
}
}
@autobind
closeFormulaPicker() {
this.setState({formulaPickerOpen: false});
}
@autobind
handleConfirm(value: any) {
const {expressionBrace} = this.state;
// 去除可能包裹的最外层的${}
value = value.replace(/^\$\{(.*)\}$/, (match: string, p1: string) => p1);
value = value ? `\${${value}}` : value;
this.editorPlugin?.insertContent(value, 'expression', expressionBrace);
this.setState({
formulaPickerOpen: false,
expressionBrace: undefined
});
}
@autobind
handleOnChange(value: any) {
this.checkOpenFormulaPicker(value);
this.props.onChange?.(value);
}
// 检测用户输入'${}'自动打开表达式弹窗
checkOpenFormulaPicker(value: string) {
const preLength = this.props.value?.length || 0;
// 删除了文本,无需检测
if (value.length < preLength) {
return;
}
let left = 0;
let right = 0;
let length = value.length;
while (
left < preLength &&
value.charAt(left) === this.props.value.charAt(left)
) {
left++;
}
while (
right < preLength &&
value.charAt(length - 1 - right) ===
this.props.value.charAt(preLength - 1 - right)
) {
right++;
}
if (preInputLocation?.end !== left) {
preInputLocation = null;
}
const start = preInputLocation ? preInputLocation.start : left;
const end = left === length - right ? left + 1 : length - right;
const inputText = value.substring(start, end);
if (/\$|\{|\}$/.test(inputText)) {
if (/\$\{\}/.test(inputText)) {
const newValue =
value.slice(0, start) +
inputText.replace('${}', '') +
value.slice(end);
this.props.onChange(newValue);
const corsur = this.editorPlugin.getCorsur();
this.setState({
formulaPickerOpen: true,
formulaPickerValue: '',
expressionBrace: [
{
line: corsur?.line,
ch: end - 3
},
{
line: corsur?.line,
ch: end
}
]
});
preInputLocation = null;
} else {
preInputLocation = {
start: left,
...preInputLocation,
end
};
}
} else {
preInputLocation = null;
}
}
@autobind
handleClear() {
this.editorPlugin?.setValue('');
}
@autobind
handleFormulaClick() {
this.setState({
formulaPickerOpen: true,
formulaPickerValue: '',
expressionBrace: undefined
});
}
@autobind
editorFactory(dom: HTMLElement, cm: any) {
return editorFactory(dom, cm, this.props.value, {
lineWrapping: false,
cursorHeight: 0.85
});
}
@autobind
handleEditorMounted(cm: any, editor: any) {
const variables = this.props.variables || this.state.variables;
this.editorPlugin = new FormulaPlugin(editor, {
getProps: () => ({...this.props, variables}),
onExpressionClick: this.onExpressionClick,
onExpressionMouseEnter: this.onExpressionMouseEnter,
showPopover: false
});
}
@autobind
editorAutoMark() {
this.editorPlugin?.autoMark();
}
render() {
const {
className,
header,
label,
placeholder,
customFormulaPicker,
clearable,
...rest
} = this.props;
const {formulaPickerOpen, formulaPickerValue, variables, tooltipStyle} =
this.state;
const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker;
const highlightValue = FormulaEditor.highlightValue(
formulaPickerValue,
variables
) || {
html: formulaPickerValue
};
return (
<div
className={cx('ae-TplFormulaControl', className, {
clearable: clearable
})}
ref={this.wrapRef}
>
<div className={cx('ae-TplResultBox')}>
<CodeMirrorEditor
className="ae-TplResultBox-editor"
value={this.props.value}
onChange={this.handleOnChange}
editorFactory={this.editorFactory}
editorDidMount={this.handleEditorMounted}
onBlur={this.editorAutoMark}
/>
{!this.props.value && (
<div className="ae-TplFormulaControl-placeholder">
{placeholder}
</div>
)}
{clearable && this.props.value && (
<Icon
icon="input-clear"
className="input-clear-icon"
iconContent="InputText-clear"
onClick={this.handleClear}
/>
)}
</div>
<Button
className="ae-TplFormulaControl-button"
size="sm"
tooltip={{
enterable: false,
content: '点击配置表达式',
tooltipTheme: 'dark',
placement: 'left',
mouseLeaveDelay: 0
}}
onClick={this.handleFormulaClick}
>
<Icon
icon="input-add-fx"
className={cx('ae-TplFormulaControl-icon', 'icon')}
/>
</Button>
<TooltipWrapper
trigger="hover"
placement="top"
style={{fontSize: '12px'}}
tooltip={{
tooltipTheme: 'dark',
children: () => renderFormulaValue(highlightValue)
}}
>
<div
className="ae-TplFormulaControl-tooltip"
style={tooltipStyle}
ref={this.tooltipRef}
onClick={() => {
this.setState({formulaPickerOpen: true});
}}
></div>
</TooltipWrapper>
{formulaPickerOpen ? (
<FormulaPickerCmp
{...this.props}
value={formulaPickerValue}
initable={true}
header={header}
variables={variables}
variableMode={rest.variableMode}
evalMode={true}
onClose={this.closeFormulaPicker}
onConfirm={this.handleConfirm}
/>
) : null}
</div>
);
}
}
@FormItem({
type: 'ae-tplFormulaControl'
})
export default class TplFormulaControlRenderer extends TplFormulaControl {}

View File

@ -11,6 +11,10 @@ export interface FormulaPickerProps {
initable?: boolean;
variableMode?: 'tabs' | 'tree';
evalMode?: boolean;
/**
* "表达式"
*/
header: string;
}
export interface CustomFormulaPickerProps extends FormulaPickerProps {
@ -48,9 +52,9 @@ const FormulaPicker: React.FC<FormulaPickerProps> = props => {
closeOnEsc
>
<Modal.Body>
<FormulaEditor
<FormulaEditor
{...props}
header="表达式"
header={props.header || '表达式'}
variables={variables}
variableMode={variableMode}
value={formula}

View File

@ -2,20 +2,53 @@
* @file
*/
import React from 'react';
import React, {MouseEvent} from 'react';
import cx from 'classnames';
import {Icon, FormItem} from 'amis';
import {autobind, FormControlProps, Schema} from 'amis-core';
import CodeMirrorEditor from 'amis-ui/lib/components/CodeMirror';
import {Icon, FormItem, TooltipWrapper} from 'amis';
import {autobind, FormControlProps, render as renderAmis} from 'amis-core';
import {CodeMirrorEditor, FormulaEditor} from 'amis-ui';
import type {VariableItem, CodeMirror} from 'amis-ui';
import {FormulaPlugin, editorFactory} from './plugin';
import FormulaPicker, {CustomFormulaPickerProps} from './FormulaPicker';
import debounce from 'lodash/debounce';
import CodeMirror from 'codemirror';
import {getVariables} from './utils';
import {VariableItem} from 'amis-ui/lib/components/formula/Editor';
import {reaction} from 'mobx';
import {renderFormulaValue} from '../FormulaControl';
export interface AdditionalMenuClickOpts {
/**
*
*/
value: string;
/**
*
* @param value
* @returns
*/
setValue: (value: string) => void;
/**
*
* @param content
* @param type expression string
* @param brace
* @returns
*/
insertContent: (
content: string,
type: 'expression' | 'string',
brace?: Array<CodeMirror.Position>
) => void;
}
export interface AdditionalMenu {
label: string; // 文案当存在图标时为tooltip内容
onClick?: (
e: MouseEvent<HTMLAnchorElement>,
opts: AdditionalMenuClickOpts
) => void; // 触发事件
icon?: string; // 图标
className?: string; //外层类名
}
export interface TextareaFormulaControlProps extends FormControlProps {
/**
* 100 px
@ -41,12 +74,7 @@ export interface TextareaFormulaControlProps extends FormControlProps {
/**
*
*/
additionalMenus?: Array<{
label: string; // 文案当存在图标时为tooltip内容
onClick: () => void; // 触发事件
icon?: string; // 图标
className?: string; //外层类名
}>;
additionalMenus?: Array<AdditionalMenu>;
/**
*
@ -57,11 +85,14 @@ export interface TextareaFormulaControlProps extends FormControlProps {
* fx面板
*/
customFormulaPicker?: React.FC<CustomFormulaPickerProps>;
/**
* "表达式"
*/
header: string;
}
interface TextareaFormulaControlState {
value: string; // 当前文本值
variables: Array<VariableItem>; // 变量数据
formulaPickerOpen: boolean; // 是否打开公式编辑器
@ -71,6 +102,8 @@ interface TextareaFormulaControlState {
expressionBrace?: Array<CodeMirror.Position>; // 表达式所在位置
isFullscreen: boolean; //是否全屏
tooltipStyle: {[key: string]: string}; // 提示框样式
}
export class TextareaFormulaControl extends React.Component<
@ -80,12 +113,15 @@ export class TextareaFormulaControl extends React.Component<
static defaultProps: Partial<TextareaFormulaControlProps> = {
variableMode: 'tabs',
requiredDataPropsVariables: false,
height: 100
height: 100,
placeholder: '请输入'
};
isUnmount: boolean;
wrapRef = React.createRef<HTMLDivElement>();
editorPlugin?: FormulaPlugin;
tooltipRef = React.createRef<HTMLDivElement>();
editorPlugin: FormulaPlugin;
unReaction: any;
appLocale: string;
appCorpusData: any;
@ -93,11 +129,11 @@ export class TextareaFormulaControl extends React.Component<
constructor(props: TextareaFormulaControlProps) {
super(props);
this.state = {
value: '',
variables: [],
formulaPickerOpen: false,
formulaPickerValue: '',
isFullscreen: false
isFullscreen: false,
tooltipStyle: {}
};
}
@ -117,6 +153,13 @@ export class TextareaFormulaControl extends React.Component<
}
);
if (this.tooltipRef.current) {
this.tooltipRef.current.addEventListener(
'mouseleave',
this.hiddenToolTip
);
}
const variablesArr = await getVariables(this);
this.setState({
variables: variablesArr
@ -133,7 +176,14 @@ export class TextareaFormulaControl extends React.Component<
}
componentWillUnmount() {
this.isUnmount = true;
if (this.tooltipRef.current) {
this.tooltipRef.current.removeEventListener(
'mouseleave',
this.hiddenToolTip
);
}
this.editorPlugin?.dispose();
this.unReaction?.();
}
@ -146,6 +196,41 @@ export class TextareaFormulaControl extends React.Component<
});
}
@autobind
onExpressionMouseEnter(
e: any,
expression: string,
brace?: Array<CodeMirror.Position>
) {
const wrapperRect = this.wrapRef.current?.getBoundingClientRect();
const expressionRect = (
e.target as HTMLSpanElement
).getBoundingClientRect();
if (!wrapperRect) {
return;
}
const left = expressionRect.left - wrapperRect.left;
const top = expressionRect.top - wrapperRect.top;
this.setState({
tooltipStyle: {
left: `${left}px`,
top: `${top}px`,
width: `${expressionRect.width}px`
},
formulaPickerValue: expression,
expressionBrace: brace
});
}
@autobind
hiddenToolTip() {
this.setState({
tooltipStyle: {
display: 'none'
}
});
}
@autobind
closeFormulaPicker() {
this.setState({formulaPickerOpen: false});
@ -162,28 +247,25 @@ export class TextareaFormulaControl extends React.Component<
formulaPickerOpen: false,
expressionBrace: undefined
});
this.closeFormulaPicker();
}
handleOnChange = debounce((value: any) => {
@autobind
handleOnChange(value: any) {
this.props.onChange?.(value);
}, 200);
}
@autobind
editorFactory(dom: HTMLElement, cm: any) {
const variables = this.props.variables || this.state.variables;
return editorFactory(dom, cm, {...this.props, variables});
return editorFactory(dom, cm, this.props.value);
}
@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, {
getProps: () => ({...this.props, variables}),
onExpressionClick: this.onExpressionClick,
onExpressionMouseEnter: this.onExpressionMouseEnter
});
}
@autobind
@ -213,6 +295,18 @@ export class TextareaFormulaControl extends React.Component<
this.editorPlugin?.autoMark();
}
@autobind
handleAddtionalMenuClick(
e: MouseEvent<HTMLAnchorElement>,
item: AdditionalMenu
) {
item.onClick?.(e, {
value: this.props.value || '',
setValue: this.editorPlugin.setValue,
insertContent: this.editorPlugin.insertContent
});
}
render() {
const {
className,
@ -226,11 +320,11 @@ export class TextareaFormulaControl extends React.Component<
...rest
} = this.props;
const {
value,
formulaPickerOpen,
formulaPickerValue,
isFullscreen,
variables
variables,
tooltipStyle
} = this.state;
const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker;
@ -241,6 +335,13 @@ export class TextareaFormulaControl extends React.Component<
resultBoxStyle.height = `${height}px`;
}
const highlightValue = FormulaEditor.highlightValue(
formulaPickerValue,
variables
) || {
html: formulaPickerValue
};
return (
<div
className={cx(
@ -250,16 +351,22 @@ export class TextareaFormulaControl extends React.Component<
},
className
)}
ref={this.wrapRef}
>
<div className={cx('ae-TextareaResultBox')} style={resultBoxStyle}>
<CodeMirrorEditor
className="ae-TextareaResultBox-editor"
value={value}
value={this.props.value}
onChange={this.handleOnChange}
editorFactory={this.editorFactory}
editorDidMount={this.handleEditorMounted}
onBlur={this.editorAutoMark}
/>
{!this.props.value && (
<div className="ae-TextareaResultBox-placeholder">
{placeholder}
</div>
)}
<ul className="ae-TextareaResultBox-footer">
<li className="ae-TextareaResultBox-footer-fullscreen">
<a
@ -280,24 +387,31 @@ export class TextareaFormulaControl extends React.Component<
data-position="top"
onClick={this.handleFormulaClick}
>
<Icon icon="function" className="icon" />
<Icon icon="input-add-fx" className="icon" />
</a>
</li>
{/* 附加底部按钮菜单项 */}
{additionalMenus?.length &&
additionalMenus?.map((item, i) => {
return (
<li key={i} className={item?.className || ''}>
<li key={i}>
{item.icon ? (
<a
data-tooltip={item.label}
data-position="top"
onClick={() => item.onClick()}
onClick={e => this.handleAddtionalMenuClick(e, item)}
>
<Icon icon={item.icon} className="icon" />
{renderAmis({
type: 'icon',
icon: item.icon,
vendor: '',
className: item.className
})}
</a>
) : (
<a onClick={() => item?.onClick()}>{item.label}</a>
<a onClick={e => this.handleAddtionalMenuClick(e, item)}>
{item.label}
</a>
)}
</li>
);
@ -312,15 +426,35 @@ export class TextareaFormulaControl extends React.Component<
></div>
) : null}
<TooltipWrapper
trigger="hover"
placement="top"
style={{fontSize: '12px'}}
tooltip={{
tooltipTheme: 'dark',
children: () => renderFormulaValue(highlightValue)
}}
>
<div
className="ae-TplFormulaControl-tooltip"
style={tooltipStyle}
ref={this.tooltipRef}
onClick={() => {
this.setState({formulaPickerOpen: true});
}}
></div>
</TooltipWrapper>
{formulaPickerOpen ? (
<FormulaPickerCmp
{...this.props}
value={formulaPickerValue}
initable={true}
variables={variables}
header={header}
variableMode={rest.variableMode}
evalMode={true}
onClose={() => this.setState({formulaPickerOpen: false})}
onClose={this.closeFormulaPicker}
onConfirm={this.handleConfirm}
/>
) : null}

View File

@ -2,37 +2,60 @@
* @file codemirror
*/
import type CodeMirror from 'codemirror';
import {TextareaFormulaControlProps} from './TextareaFormulaControl';
import {FormulaEditor} from 'amis-ui/lib/components/formula/Editor';
import {FormulaEditor} from 'amis-ui';
import type {VariableItem, CodeMirror} from 'amis-ui';
export function editorFactory(
dom: HTMLElement,
cm: typeof CodeMirror,
props: any
value: string,
config?: Object
) {
return cm(dom, {
value: props.value || '',
value: value || '',
autofocus: false,
lineWrapping: true
lineWrapping: true,
...config
});
}
interface FormulaPluginConfig {
getProps: () => TextareaFormulaControlProps;
onExpressionClick?: (
expression: string,
brace?: Array<CodeMirror.Position>
) => any;
onExpressionMouseEnter?: (
e: MouseEvent,
expression: string,
brace?: Array<CodeMirror.Position>
) => any;
showPopover?: boolean;
showClearIcon?: boolean; // 表达式是否展示删除icon
}
const defaultPluginConfig = {
showPopover: false,
showClearIcon: false
};
export class FormulaPlugin {
constructor(
readonly editor: CodeMirror.Editor,
readonly cm: typeof CodeMirror,
readonly getProps: () => TextareaFormulaControlProps,
readonly onExpressionClick: (
expression: string,
brace?: Array<CodeMirror.Position>
) => any
) {
const {value} = this.getProps();
config: FormulaPluginConfig;
constructor(readonly editor: CodeMirror.Editor, config: FormulaPluginConfig) {
this.config = {
...defaultPluginConfig,
...config
};
const {value} = this.config.getProps();
if (value) {
this.autoMark();
this.focus(value);
}
this.setValue = this.setValue.bind(this);
this.insertContent = this.insertContent.bind(this);
}
autoMark() {
@ -154,27 +177,36 @@ export class FormulaPlugin {
}
}
// 重新赋值
setValue(value: string) {
this.editor.setValue(value);
}
getCorsur() {
return this.editor.getCursor();
}
insertContent(
value: any,
type?: 'expression',
content: string,
type: 'expression' | 'string' = 'string',
brace?: Array<CodeMirror.Position>
) {
if (brace) {
// 替换
const [from, to] = brace;
if (type === 'expression') {
this.editor.replaceRange(value, from, to);
this.editor.replaceRange(content, from, to);
this.autoMark();
} else if (typeof value === 'string') {
this.editor.replaceRange(value, from, to);
} else if (type === 'string') {
this.editor.replaceRange(content, from, to);
}
} else {
// 新增
if (type === 'expression') {
this.editor.replaceSelection(value);
this.editor.replaceSelection(content);
this.autoMark();
} else if (typeof value === 'string') {
this.editor.replaceSelection(value);
} else if (type === 'string') {
this.editor.replaceSelection(content);
}
this.editor.focus();
}
@ -186,33 +218,59 @@ export class FormulaPlugin {
expression = '',
className = 'cm-expression'
) {
const wrap = document.createElement('span');
wrap.className = className;
const text = document.createElement('span');
text.className = `${className}-text`;
text.innerText = expression;
text.setAttribute('data-expression', expression);
text.onclick = () => {
const brace = this.getExpressionBrace(expression);
this.onExpressionClick(expression, brace);
};
const {variables} = this.getProps();
const {
onExpressionClick,
onExpressionMouseEnter,
getProps,
showPopover,
showClearIcon
} = this.config;
const variables = getProps()?.variables as VariableItem[];
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');
const arrow = document.createElement('div');
arrow.classList.add('expression-popover-arrow');
popoverEl.appendChild(arrow);
const wrap = document.createElement('span');
wrap.className = className;
const text = document.createElement('span');
text.className = `${className}-text`;
text.innerHTML = highlightValue.html;
text.setAttribute('data-expression', expression);
text.onclick = () => {
const brace = this.getExpressionBrace(expression);
onExpressionClick?.(expression, brace);
};
text.onmouseenter = e => {
const brace = this.getExpressionBrace(expression);
onExpressionMouseEnter?.(e, expression, brace);
};
wrap.appendChild(text);
wrap.appendChild(popoverEl);
if (showClearIcon) {
const closeIcon = document.createElement('i');
closeIcon.className = 'cm-expression-close iconfont icon-close';
closeIcon.onclick = () => {
const brace = this.getExpressionBrace(expression);
this.insertContent('', 'expression', brace);
};
wrap.appendChild(closeIcon);
}
if (showPopover) {
// 添加popover
const popoverEl = document.createElement('div');
// bca-disable-next-line
popoverEl.innerHTML = highlightValue.html;
popoverEl.classList.add('cm-expression-popover');
const arrow = document.createElement('div');
arrow.classList.add('cm-expression-popover-arrow');
popoverEl.appendChild(arrow);
wrap.appendChild(popoverEl);
}
this.editor.markText(from, to, {
atomic: true,

View File

@ -130,6 +130,13 @@ setSchemaTpl('textareaFormulaControl', (schema: object = {}) => {
};
});
setSchemaTpl('tplFormulaControl', (schema: object = {}) => {
return {
type: 'ae-tplFormulaControl',
...schema
};
});
setSchemaTpl('DataPickerControl', (schema: object = {}) => {
return {
type: 'ae-DataPickerControl',

View File

@ -45,6 +45,7 @@
var(--input-default-default-paddingLeft);
background: var(--input-default-default-bg-color);
height: var(--input-size-default-height);
align-items: center;
&:hover,
&.hover {

View File

@ -70,6 +70,8 @@ import SchemaVariableListPicker from './schema-editor/SchemaVariableListPicker';
import SchemaVariableList from './schema-editor/SchemaVariableList';
import VariableList from './formula/VariableList';
import FormulaPicker from './formula/Picker';
import {FormulaEditor} from './formula/Editor';
import type {VariableItem} from './formula/Editor';
import PickerContainer from './PickerContainer';
import InputJSONSchema from './json-schema';
import {Badge, withBadge} from './Badge';
@ -122,6 +124,8 @@ import ConfirmBox from './ConfirmBox';
import DndContainer from './DndContainer';
import Menu from './menu';
import InputBoxWithSuggestion from './InputBoxWithSuggestion';
import {CodeMirrorEditor} from './CodeMirror';
import type CodeMirror from 'codemirror';
export {
NotFound,
@ -193,6 +197,8 @@ export {
PickerContainer,
ConfirmBox,
FormulaPicker,
VariableItem,
FormulaEditor,
InputJSONSchema,
withBadge,
BadgeObject,
@ -248,5 +254,7 @@ export {
InputTable,
InputTableColumnProps,
DndContainer,
Menu
Menu,
CodeMirror,
CodeMirrorEditor
};