feat: Textarea/Editor/DiffEditor动作事件扩充

This commit is contained in:
lurunze1226 2022-02-15 17:31:06 +08:00
parent 97c624ae48
commit 1d46d6a4c7
7 changed files with 385 additions and 89 deletions

View File

@ -11,6 +11,7 @@ export default {
},
{
type: 'form',
name: 'text-form',
debug: true,
api: '/api/mock2/form/saveForm',
body: [
@ -30,6 +31,11 @@ export default {
actionType: 'clear',
componentId: 'clear-receiver',
description: '点击清空指定输入框的内容'
},
{
actionType: 'toast',
msgType: 'info',
msg: '派发clear事件'
}
]
}
@ -39,6 +45,7 @@ export default {
name: 'clear-receiver',
id: 'clear-receiver',
type: 'input-text',
clearable: true,
label: 'clear动作测试',
mode: 'row',
value: 'chunk of text ready to be cleared.'
@ -61,6 +68,11 @@ export default {
actionType: 'focus',
componentId: 'focus-receiver',
description: '点击使指定输入框聚焦'
},
{
actionType: 'toast',
msgType: 'info',
msg: '派发focus事件'
}
]
}
@ -76,6 +88,191 @@ export default {
]
}
]
},
{
type: 'divider'
},
{
type: 'tpl',
tpl: 'Textarea 多行文本输入框',
inline: false,
wrapperComponent: 'h2'
},
{
type: 'form',
name: 'textarea-form',
debug: true,
api: '/api/mock2/form/saveForm',
body: [
{
type: 'group',
body: [
{
name: 'textarea-trigger1',
id: 'textarea-trigger1',
type: 'action',
label: 'clear触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'textarea-clear-receiver',
description: '点击清空指定输入框的内容'
},
{
actionType: 'toast',
msgType: 'info',
msg: '派发clear事件'
}
]
}
}
},
{
name: 'textarea-clear-receiver',
id: 'textarea-clear-receiver',
type: 'textarea',
clearable: true,
label: 'clear动作测试',
mode: 'row',
value: 'chunk of text ready to be cleared.'
}
]
},
{
type: 'group',
body: [
{
name: 'textarea-trigger2',
id: 'textarea-trigger2',
type: 'action',
label: 'focus触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'focus',
componentId: 'textarea-focus-receiver',
description: '点击使指定输入框聚焦'
},
{
actionType: 'toast',
msgType: 'info',
msg: '派发focus事件'
}
]
}
}
},
{
name: 'textarea-focus-receiver',
id: 'textarea-focus-receiver',
type: 'textarea',
label: 'focus动作测试',
mode: 'row'
}
]
}
]
},
{
type: 'divider'
},
{
type: 'tpl',
tpl: 'Editor 编辑器',
inline: false,
wrapperComponent: 'h2'
},
{
type: 'form',
name: 'editor-form',
debug: true,
api: '/api/mock2/form/saveForm',
body: [
{
type: 'group',
mode: 'inline',
body: [
{
name: 'editor-trigger2',
id: 'editor-trigger2',
type: 'action',
label: '编辑器focus触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'focus',
componentId: 'editor-focus-receiver',
description: '点击使指定输入框聚焦'
},
{
actionType: 'toast',
msgType: 'info',
msg: '派发focus事件'
}
]
}
}
},
{
name: 'editor-focus-receiver',
id: 'editor-focus-receiver',
type: 'editor',
language: 'javascript',
label: '编辑器focus动作测试',
value:
"function HelloWorld() {\n console.log('Hello World');\n}",
mode: 'row'
}
]
},
{
type: 'group',
mode: 'inline',
body: [
{
name: 'diffeditor-trigger2',
id: 'diffeditor-trigger2',
type: 'action',
label: '对比编辑器focus触发器',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'focus',
componentId: 'diffeditor-focus-receiver',
description: '点击使指定输入框聚焦'
},
{
actionType: 'toast',
msgType: 'info',
msg: '派发focus事件'
}
]
}
}
},
{
name: 'diffeditor-focus-receiver',
id: 'diffeditor-focus-receiver',
type: 'diff-editor',
label: '对比编辑器focus动作测试',
diffValue:
"function HelloWorld() {\n console.log('Hello World');\n}",
value:
"function HelloWorld() {\n console.log('Hello World!');\n}",
mode: 'row'
}
]
}
]
}
]
};

86
src/actions/Decorators.ts Normal file
View File

@ -0,0 +1,86 @@
import {createObject} from '../utils/helper';
import type {ListenerAction} from './Action';
import type {OptionsControlProps} from '../renderers/Form/Options';
import type {FormControlProps} from '../renderers/Form/Item';
import type {RendererEvent} from '../utils/renderer-event';
/**
*
*
* @param props props
* @param e
* @param ctx
*/
export async function rendererEventDispatcher<
T extends FormControlProps,
E = any
>(
props: T,
e: E,
ctx: Record<string, any> = {}
): Promise<RendererEvent<any> | undefined> {
const {dispatchEvent, data} = props;
return dispatchEvent(e, createObject(data, ctx));
}
/**
*
*
* @param event
* @param ctx
* @returns {Function}
*/
export function bindRendererEvent<T extends FormControlProps, E = any>(
event: E,
ctx: Record<string, any> = {}
) {
return function (
target: any,
propertyKey: string,
descriptor: TypedPropertyDescriptor<any>
) {
let fn =
descriptor.value && typeof descriptor.value === 'function'
? descriptor.value
: typeof descriptor?.get === 'function'
? descriptor.get()
: null;
if (!fn || typeof fn !== 'function') {
throw new Error(
`decorator can only be applied to methods not: ${typeof fn}`
);
}
return {
...descriptor,
value: async function boundFn(...params: any[]) {
const triggerProps = (this as TypedPropertyDescriptor<any> & {props: T})
?.props;
let value = triggerProps?.value;
// clear清除内容事件
if (typeof event === 'string' && event === 'clear') {
value = triggerProps?.resetValue;
}
const dispatcher = await rendererEventDispatcher<T>(
triggerProps,
event,
{
value
}
);
if (dispatcher?.prevented) {
return;
}
return fn.apply(this, [...params]);
}
};
};
}

View File

@ -17,5 +17,6 @@ import './DrawerAction';
import './EmailAction';
import './LinkAction';
import './ToastAction';
import './Decorators';
export * from './Action';

View File

@ -8,6 +8,10 @@ import {
} from '../../utils/tpl-builtin';
import {SchemaTokenizeableString} from '../../Schema';
import {autobind} from '../../utils/helper';
import {bindRendererEvent} from '../../actions/Decorators';
import type {Position} from 'monaco-editor';
import type {ListenerAction} from '../../actions/Action';
/**
* Diff
@ -35,6 +39,8 @@ export interface DiffControlSchema extends FormBaseControl {
options?: any;
}
export type DiffEditorRendererEvent = 'blur' | 'focus';
function loadComponent(): Promise<any> {
return import('../../components/Editor').then(item => item.default);
}
@ -106,12 +112,31 @@ export class DiffEditor extends React.Component<DiffEditorProps, any> {
this.toDispose.forEach(fn => fn());
}
doAction(action: ListenerAction, args: any) {
const actionType = action?.actionType as string;
if (actionType === 'focus') {
this.focus();
}
}
focus() {
this.editor.focus();
this.setState({focused: true});
// 最近一次光标位置
const position: Position | null = this.editor?.getPosition();
this.editor?.setPosition(position);
}
@bindRendererEvent<DiffEditorProps, DiffEditorRendererEvent>('focus')
handleFocus() {
this.setState({
focused: true
});
}
@bindRendererEvent<DiffEditorProps, DiffEditorRendererEvent>('blur')
handleBlur() {
this.setState({
focused: false

View File

@ -8,6 +8,10 @@ import {
isPureVariable,
resolveVariableAndFilter
} from '../../utils/tpl-builtin';
import {bindRendererEvent} from '../../actions/Decorators';
import type {Position} from 'monaco-editor';
import type {ListenerAction} from '../../actions/Action';
/**
* Editor
@ -110,6 +114,8 @@ export interface EditorControlSchema extends Omit<FormBaseControl, 'size'> {
allowFullscreen?: boolean;
}
export type EditorRendererEvent = 'blur' | 'focus';
export interface EditorProps extends FormControlProps {
options?: object;
}
@ -148,12 +154,31 @@ export default class EditorControl extends React.Component<EditorProps, any> {
this.toDispose.forEach(fn => fn());
}
doAction(action: ListenerAction, args: any) {
const actionType = action?.actionType as string;
if (actionType === 'focus') {
this.focus();
}
}
focus() {
this.editor.focus();
this.setState({focused: true});
// 最近一次光标位置
const position: Position | null = this.editor?.getPosition();
this.editor?.setPosition(position);
}
@bindRendererEvent<EditorProps, EditorRendererEvent>('focus')
handleFocus() {
this.setState({
focused: true
});
}
@bindRendererEvent<EditorProps, EditorRendererEvent>('blur')
handleBlur() {
this.setState({
focused: false

View File

@ -20,10 +20,12 @@ import {FormBaseControl} from './Item';
import {ActionSchema} from '../Action';
import {SchemaApi} from '../../Schema';
import {generateIcon} from '../../utils/icon';
import {
rendererEventDispatcher,
bindRendererEvent
} from '../../actions/Decorators';
import type {Option} from '../../components/Select';
import type {RendererEvent} from '../../utils/renderer-event';
import type {IScopedContext} from '../../Scoped';
import type {ListenerAction} from '../../actions/Action';
// declare function matchSorter(items:Array<any>, input:any, options:any): Array<any>;
@ -86,6 +88,14 @@ export interface TextControlSchema extends FormOptionsControl {
suffix?: string;
}
export type InputTextRendererEvent =
| 'blur'
| 'focus'
| 'click'
| 'change'
| 'clear'
| 'enter';
export interface TextProps extends OptionsControlProps {
placeholder?: string;
addOn?: Action & {
@ -112,71 +122,6 @@ export interface TextState {
isFocused?: boolean;
}
export type InputTextRendererEvent =
| 'blur'
| 'focus'
| 'click'
| 'change'
| 'clear'
| 'enter';
/**
*
*/
async function rendererEventDispatcher<T extends OptionsControlProps>(
props: T,
e: InputTextRendererEvent,
ctx: Record<string, any> = {}
): Promise<RendererEvent<any> | undefined> {
const {dispatchEvent, data} = props;
return dispatchEvent(e, createObject(data, ctx));
}
/**
*
*
* @param {InputTextRendererEvent} e
* @returns {Function}
*/
export function bindRendererEvent<T extends OptionsControlProps, P = any>(
e: InputTextRendererEvent,
ctx: Record<string, any> = {}
) {
return function (
target: any,
propertyKey: string,
descriptor: TypedPropertyDescriptor<any>
) {
let fn = descriptor.value;
if (!fn || typeof fn !== 'function') {
throw new Error(
`decorator can only be applied to methods not: ${typeof fn}`
);
}
return {
...descriptor,
value: async function boundFn(...params: any[]) {
const context = (this as TypedPropertyDescriptor<any> & {props: T})
?.props;
let value = e === 'clear' ? context?.resetValue : context?.value;
const dispatcher = await rendererEventDispatcher<T>(context, e, {
value
});
if (dispatcher?.prevented) {
return;
}
return fn.apply(this, [...params]);
}
};
};
}
export default class TextControl extends React.PureComponent<
TextProps,
TextState
@ -272,9 +217,6 @@ export default class TextControl extends React.PureComponent<
this.input = ref;
}
/**
*
*/
doAction(action: ListenerAction, args: any) {
const actionType = action?.actionType as string;
@ -297,7 +239,7 @@ export default class TextControl extends React.PureComponent<
len && this.input.setSelectionRange(len, len);
}
@bindRendererEvent<TextProps>('clear')
@bindRendererEvent<TextProps, InputTextRendererEvent>('clear')
clearValue() {
const {onChange, resetValue} = this.props;
@ -322,7 +264,7 @@ export default class TextControl extends React.PureComponent<
onChange(this.normalizeValue(newValue));
}
@bindRendererEvent<TextProps>('click')
@bindRendererEvent<TextProps, InputTextRendererEvent>('click')
handleClick() {
this.focus();
this.setState({
@ -330,7 +272,7 @@ export default class TextControl extends React.PureComponent<
});
}
@bindRendererEvent<TextProps>('focus')
@bindRendererEvent<TextProps, InputTextRendererEvent>('focus')
handleFocus(e: any) {
this.setState({
isOpen: true,
@ -340,7 +282,7 @@ export default class TextControl extends React.PureComponent<
this.props.onFocus && this.props.onFocus(e);
}
@bindRendererEvent<TextProps>('blur')
@bindRendererEvent<TextProps, InputTextRendererEvent>('blur')
handleBlur(e: any) {
const {onBlur, trimContents, value, onChange} = this.props;
@ -415,11 +357,10 @@ export default class TextControl extends React.PureComponent<
value = this.normalizeValue(newValue).concat();
}
const dispatcher = await rendererEventDispatcher<TextProps>(
this.props,
'enter',
{value}
);
const dispatcher = await rendererEventDispatcher<
TextProps,
InputTextRendererEvent
>(this.props, 'enter', {value});
if (dispatcher?.prevented) {
return;
@ -513,11 +454,10 @@ export default class TextControl extends React.PureComponent<
async handleNormalInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const {onChange} = this.props;
let value = e.currentTarget.value;
const dispatcher = await rendererEventDispatcher<TextProps>(
this.props,
'change',
{value: this.transformValue(value)}
);
const dispatcher = await rendererEventDispatcher<
TextProps,
InputTextRendererEvent
>(this.props, 'change', {value: this.transformValue(value)});
if (dispatcher?.prevented) {
return;

View File

@ -5,6 +5,10 @@ import Textarea from '../../components/Textarea';
import {Icon} from '../../components/icons';
import {findDOMNode} from 'react-dom';
import {autobind, ucFirst} from '../../utils/helper';
import {bindRendererEvent} from '../../actions/Decorators';
import type {ListenerAction} from '../../actions/Action';
/**
* TextArea
* https://baidu.gitee.io/amis/docs/components/form/textarea
@ -56,6 +60,8 @@ export interface TextareaControlSchema extends FormBaseControl {
resetValue?: string;
}
export type TextAreaRendererEvent = 'blur' | 'focus' | 'clear';
export interface TextAreaProps extends FormControlProps {
placeholder?: string;
minRows?: number;
@ -64,9 +70,13 @@ export interface TextAreaProps extends FormControlProps {
resetValue?: string;
}
export interface TextAreaState {
focused: boolean;
}
export default class TextAreaControl extends React.Component<
TextAreaProps,
{focused: boolean}
TextAreaState
> {
static defaultProps: Partial<TextAreaProps> = {
minRows: 3,
@ -83,6 +93,16 @@ export default class TextAreaControl extends React.Component<
input?: HTMLInputElement;
inputRef = (ref: any) => (this.input = findDOMNode(ref) as HTMLInputElement);
doAction(action: ListenerAction, args: any) {
const actionType = action?.actionType as string;
if (!!~['clear', 'reset'].indexOf(actionType)) {
this.handleClear();
} else if (actionType === 'focus') {
this.focus();
}
}
valueToString(value: any) {
return typeof value === 'undefined' || value === null
? ''
@ -115,15 +135,15 @@ export default class TextAreaControl extends React.Component<
}
@autobind
handleChange(e: React.ChangeEvent<any>) {
handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
const {onChange} = this.props;
let value = e.currentTarget.value;
onChange(value);
onChange?.(value);
}
@autobind
@bindRendererEvent<TextAreaProps, TextAreaRendererEvent>('focus')
handleFocus(e: React.FocusEvent<HTMLTextAreaElement>) {
const {onFocus} = this.props;
@ -138,6 +158,7 @@ export default class TextAreaControl extends React.Component<
}
@autobind
@bindRendererEvent<TextAreaProps, TextAreaRendererEvent>('blur')
handleBlur(e: React.FocusEvent<HTMLTextAreaElement>) {
const {onBlur, trimContents, value, onChange} = this.props;
@ -156,7 +177,8 @@ export default class TextAreaControl extends React.Component<
}
@autobind
handleClear() {
@bindRendererEvent<TextAreaProps, TextAreaRendererEvent>('clear')
async handleClear() {
const {onChange, resetValue} = this.props;
onChange?.(resetValue);