条件组合半成品
This commit is contained in:
parent
8901b57389
commit
9bb718aeac
|
@ -38,17 +38,43 @@
|
|||
}
|
||||
}
|
||||
|
||||
.#{$ns}CBItem {
|
||||
display: flex;
|
||||
margin-top: px2rem(10px);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: px2rem(30px);
|
||||
.#{$ns}CBDelete {
|
||||
@include icon-color();
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.#{$ns}CBGroupOrItem {
|
||||
position: relative;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
transition: all 0.3s ease-out;
|
||||
margin-left: px2rem(30px);
|
||||
|
||||
&-body {
|
||||
display: flex;
|
||||
margin-top: px2rem(10px);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.is-ghost > &-body:before {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba($info, 0.2);
|
||||
}
|
||||
|
||||
&-dragbar {
|
||||
cursor: move;
|
||||
|
@ -60,11 +86,16 @@
|
|||
@include icon-color();
|
||||
}
|
||||
|
||||
.#{$ns}CBGroup {
|
||||
margin-left: $gap-xs;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover &-dragbar {
|
||||
&:hover > &-body > &-dragbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
@ -117,6 +148,7 @@
|
|||
|
||||
.#{$ns}CBFunc {
|
||||
display: inline-block;
|
||||
margin-left: $gap-xs;
|
||||
|
||||
&-select {
|
||||
display: inline-block;
|
||||
|
@ -140,3 +172,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}CBValue {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-left: $gap-xs;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import {ExpressionComplex, Field, Funcs, Func, ExpressionFunc} from './types';
|
||||
import {
|
||||
ExpressionComplex,
|
||||
Field,
|
||||
Funcs,
|
||||
Func,
|
||||
ExpressionFunc,
|
||||
Type,
|
||||
FieldSimple
|
||||
} from './types';
|
||||
import React from 'react';
|
||||
import ConditionField from './Field';
|
||||
import {autobind, findTree} from '../../utils/helper';
|
||||
|
@ -20,7 +28,7 @@ export interface ExpressionProps extends ThemeProps {
|
|||
value: ExpressionComplex;
|
||||
index?: number;
|
||||
onChange: (value: ExpressionComplex, index?: number) => void;
|
||||
valueField?: Field;
|
||||
valueField?: FieldSimple;
|
||||
fields?: Field[];
|
||||
funcs?: Funcs;
|
||||
defaultType?: 'value' | 'field' | 'func' | 'raw';
|
||||
|
@ -94,7 +102,14 @@ export class Expression extends React.Component<ExpressionProps> {
|
|||
handleRawChange() {}
|
||||
|
||||
render() {
|
||||
const {value, defaultType, allowedTypes, funcs, fields} = this.props;
|
||||
const {
|
||||
value,
|
||||
valueField,
|
||||
defaultType,
|
||||
allowedTypes,
|
||||
funcs,
|
||||
fields
|
||||
} = this.props;
|
||||
const inputType =
|
||||
((value as any)?.type === 'field'
|
||||
? 'field'
|
||||
|
@ -128,7 +143,13 @@ export class Expression extends React.Component<ExpressionProps> {
|
|||
/>
|
||||
) : null}
|
||||
|
||||
{inputType === 'value' ? <Value /> : null}
|
||||
{inputType === 'value' ? (
|
||||
<Value
|
||||
field={valueField!}
|
||||
value={value}
|
||||
onChange={this.handleValueChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{inputType === 'field' ? (
|
||||
<ConditionField
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
import React from 'react';
|
||||
import {Fields, ConditionGroupValue, Funcs} from './types';
|
||||
import {ClassNamesFn} from '../../theme';
|
||||
import {ClassNamesFn, ThemeProps, themeable} from '../../theme';
|
||||
import Button from '../Button';
|
||||
import {ConditionItem} from './Item';
|
||||
import GroupOrItem from './GroupOrItem';
|
||||
import {autobind, guid} from '../../utils/helper';
|
||||
import {Config} from './config';
|
||||
import {Icon} from '../icons';
|
||||
|
||||
export interface ConditionGroupProps {
|
||||
export interface ConditionGroupProps extends ThemeProps {
|
||||
config: Config;
|
||||
value?: ConditionGroupValue;
|
||||
fields: Fields;
|
||||
funcs?: Funcs;
|
||||
index?: number;
|
||||
onChange: (value: ConditionGroupValue, index?: number) => void;
|
||||
classnames: ClassNamesFn;
|
||||
onChange: (value: ConditionGroupValue) => void;
|
||||
removeable?: boolean;
|
||||
onRemove?: (e: React.MouseEvent) => void;
|
||||
onDragStart?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
||||
getValue() {
|
||||
return {
|
||||
id: guid(),
|
||||
conjunction: 'and' as 'and',
|
||||
...this.props.value
|
||||
} as ConditionGroupValue;
|
||||
|
@ -31,7 +33,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
|||
let value = this.getValue();
|
||||
value.not = !value.not;
|
||||
|
||||
onChange(value, this.props.index);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
@autobind
|
||||
|
@ -39,7 +41,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
|||
const onChange = this.props.onChange;
|
||||
let value = this.getValue();
|
||||
value.conjunction = value.conjunction === 'and' ? 'or' : 'and';
|
||||
onChange(value, this.props.index);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
@autobind
|
||||
|
@ -54,7 +56,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
|||
value.children.push({
|
||||
id: guid()
|
||||
});
|
||||
onChange(value, this.props.index);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
@autobind
|
||||
|
@ -68,13 +70,18 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
|||
|
||||
value.children.push({
|
||||
id: guid(),
|
||||
conjunction: 'and'
|
||||
conjunction: 'and',
|
||||
children: [
|
||||
{
|
||||
id: guid()
|
||||
}
|
||||
]
|
||||
});
|
||||
onChange(value, this.props.index);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleItemChange(item: any, index?: number) {
|
||||
handleItemChange(item: any, index: number) {
|
||||
const onChange = this.props.onChange;
|
||||
let value = this.getValue();
|
||||
|
||||
|
@ -83,14 +90,36 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
|||
: [];
|
||||
|
||||
value.children.splice(index!, 1, item);
|
||||
onChange(value, this.props.index);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleItemRemove(index: number) {
|
||||
const onChange = this.props.onChange;
|
||||
let value = this.getValue();
|
||||
|
||||
value.children = Array.isArray(value.children)
|
||||
? value.children.concat()
|
||||
: [];
|
||||
|
||||
value.children.splice(index, 1);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classnames: cx, value, fields, funcs, config} = this.props;
|
||||
const {
|
||||
classnames: cx,
|
||||
value,
|
||||
fields,
|
||||
funcs,
|
||||
config,
|
||||
removeable,
|
||||
onRemove,
|
||||
onDragStart
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={cx('CBGroup')}>
|
||||
<div className={cx('CBGroup')} data-group-id={value?.id}>
|
||||
<div className={cx('CBGroup-toolbar')}>
|
||||
<div className={cx('CBGroup-toolbarLeft')}>
|
||||
<Button onClick={this.handleNotClick} size="sm" active={value?.not}>
|
||||
|
@ -114,43 +143,41 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
|
|||
</div>
|
||||
</div>
|
||||
<div className={cx('CBGroup-toolbarRight')}>
|
||||
<Button onClick={this.handleAdd} size="sm">
|
||||
<Button onClick={this.handleAdd} size="sm" className="m-r-xs">
|
||||
添加条件
|
||||
</Button>
|
||||
<Button onClick={this.handleAddGroup} size="sm" className="m-l-xs">
|
||||
<Button onClick={this.handleAddGroup} size="sm" className="m-r-xs">
|
||||
添加条件组
|
||||
</Button>
|
||||
{removeable ? (
|
||||
<a className={cx('CBDelete')} onClick={onRemove}>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Array.isArray(value?.children)
|
||||
? value!.children.map((item, index) =>
|
||||
(item as ConditionGroupValue).conjunction ? (
|
||||
<ConditionGroup
|
||||
<div className={cx('CBGroup-body')}>
|
||||
{Array.isArray(value?.children)
|
||||
? value!.children.map((item, index) => (
|
||||
<GroupOrItem
|
||||
draggable={value!.children!.length > 1}
|
||||
onDragStart={onDragStart}
|
||||
config={config}
|
||||
key={item.id}
|
||||
fields={fields}
|
||||
value={item as ConditionGroupValue}
|
||||
classnames={cx}
|
||||
index={index}
|
||||
onChange={this.handleItemChange}
|
||||
funcs={funcs}
|
||||
onRemove={this.handleItemRemove}
|
||||
/>
|
||||
) : (
|
||||
<ConditionItem
|
||||
config={config}
|
||||
key={item.id}
|
||||
fields={fields}
|
||||
value={item}
|
||||
classnames={cx}
|
||||
index={index}
|
||||
onChange={this.handleItemChange}
|
||||
funcs={funcs}
|
||||
/>
|
||||
)
|
||||
)
|
||||
: null}
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(ConditionGroup);
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import {Config} from './config';
|
||||
import {Fields, ConditionGroupValue, Funcs, ConditionValue} from './types';
|
||||
import {ThemeProps, themeable} from '../../theme';
|
||||
import React from 'react';
|
||||
import {Icon} from '../icons';
|
||||
import {autobind} from '../../utils/helper';
|
||||
import ConditionGroup from './Group';
|
||||
import ConditionItem from './Item';
|
||||
|
||||
export interface CBGroupOrItemProps extends ThemeProps {
|
||||
config: Config;
|
||||
value?: ConditionGroupValue;
|
||||
fields: Fields;
|
||||
funcs?: Funcs;
|
||||
index: number;
|
||||
draggable?: boolean;
|
||||
onChange: (value: ConditionGroupValue, index: number) => void;
|
||||
removeable?: boolean;
|
||||
onDragStart?: (e: React.MouseEvent) => void;
|
||||
onRemove?: (index: number) => void;
|
||||
}
|
||||
|
||||
export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
|
||||
@autobind
|
||||
handleItemChange(value: any) {
|
||||
this.props.onChange(value, this.props.index);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleItemRemove() {
|
||||
this.props.onRemove?.(this.props.index);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
value,
|
||||
config,
|
||||
fields,
|
||||
funcs,
|
||||
draggable,
|
||||
onDragStart
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={cx('CBGroupOrItem')} data-id={value?.id}>
|
||||
<div className={cx('CBGroupOrItem-body')}>
|
||||
{draggable ? (
|
||||
<a
|
||||
draggable
|
||||
onDragStart={onDragStart}
|
||||
className={cx('CBGroupOrItem-dragbar')}
|
||||
>
|
||||
<Icon icon="drag-bar" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
|
||||
{value?.conjunction ? (
|
||||
<ConditionGroup
|
||||
onDragStart={onDragStart}
|
||||
config={config}
|
||||
fields={fields}
|
||||
value={value as ConditionGroupValue}
|
||||
onChange={this.handleItemChange}
|
||||
funcs={funcs}
|
||||
removeable
|
||||
onRemove={this.handleItemRemove}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ConditionItem
|
||||
config={config}
|
||||
fields={fields}
|
||||
value={value as ConditionValue}
|
||||
onChange={this.handleItemChange}
|
||||
funcs={funcs}
|
||||
/>
|
||||
<a className={cx('CBDelete')} onClick={this.handleItemRemove}>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(CBGroupOrItem);
|
|
@ -8,9 +8,10 @@ import {
|
|||
Func,
|
||||
Field,
|
||||
FieldSimple,
|
||||
ExpressionField
|
||||
ExpressionField,
|
||||
OperatorType
|
||||
} from './types';
|
||||
import {ClassNamesFn} from '../../theme';
|
||||
import {ThemeProps, themeable} from '../../theme';
|
||||
import {Icon} from '../icons';
|
||||
import {autobind, findTree, noop} from '../../utils/helper';
|
||||
import Expression from './Expression';
|
||||
|
@ -21,13 +22,12 @@ import ResultBox from '../ResultBox';
|
|||
|
||||
const option2value = (item: any) => item.value;
|
||||
|
||||
export interface ConditionItemProps {
|
||||
export interface ConditionItemProps extends ThemeProps {
|
||||
config: Config;
|
||||
fields: Fields;
|
||||
funcs?: Funcs;
|
||||
index?: number;
|
||||
value: ConditionRule;
|
||||
classnames: ClassNamesFn;
|
||||
onChange: (value: ConditionRule, index?: number) => void;
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,18 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
|
|||
}
|
||||
|
||||
@autobind
|
||||
handleOperatorChange() {}
|
||||
handleOperatorChange(op: OperatorType) {
|
||||
const value = {...this.props.value, op: op};
|
||||
this.props.onChange(value, this.props.index);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleRightChange(rightValue: any) {
|
||||
const value = {...this.props.value, right: rightValue};
|
||||
const onChange = this.props.onChange;
|
||||
|
||||
onChange(value, this.props.index);
|
||||
}
|
||||
|
||||
renderLeft() {
|
||||
const {value, fields, funcs} = this.props;
|
||||
|
@ -101,7 +112,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
|
|||
) as FieldSimple;
|
||||
|
||||
if (field) {
|
||||
operators = field.operators || config.types[field.type].operators;
|
||||
operators = field.operators || config.types[field.type]?.operators;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,25 +160,92 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
|
|||
return null;
|
||||
}
|
||||
|
||||
renderItem() {
|
||||
renderRight() {
|
||||
const {value, funcs, fields} = this.props;
|
||||
|
||||
if (!value?.op) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const left = value?.left;
|
||||
let leftType = '';
|
||||
|
||||
if ((left as ExpressionFunc)?.type === 'func') {
|
||||
const func: Func = findTree(
|
||||
funcs!,
|
||||
(i: Func) => i.type === (left as ExpressionFunc).type
|
||||
) as Func;
|
||||
|
||||
if (func) {
|
||||
leftType = func.returnType;
|
||||
}
|
||||
} else if ((left as ExpressionField)?.type === 'field') {
|
||||
const field: FieldSimple = findTree(
|
||||
fields,
|
||||
(i: FieldSimple) => i.name === (left as ExpressionField).field
|
||||
) as FieldSimple;
|
||||
|
||||
if (field) {
|
||||
leftType = field.type;
|
||||
}
|
||||
}
|
||||
|
||||
if (leftType) {
|
||||
return this.renderRightWidgets(leftType, value.op!);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderRightWidgets(type: string, op: OperatorType) {
|
||||
const {funcs, value, fields, config} = this.props;
|
||||
let field = {
|
||||
...config.types[type],
|
||||
type
|
||||
} as FieldSimple;
|
||||
|
||||
if ((value?.left as ExpressionField)?.type === 'field') {
|
||||
const leftField: FieldSimple = findTree(
|
||||
fields,
|
||||
(i: FieldSimple) => i.name === (value?.left as ExpressionField).field
|
||||
) as FieldSimple;
|
||||
|
||||
if (leftField) {
|
||||
field = {
|
||||
...field,
|
||||
...leftField
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (op === 'is_empty' || op === 'is_not_empty') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Expression
|
||||
funcs={funcs}
|
||||
valueField={field}
|
||||
value={value.right}
|
||||
onChange={this.handleRightChange}
|
||||
fields={fields}
|
||||
defaultType="value"
|
||||
allowedTypes={field?.valueTypes || ['value', 'field', 'func', 'raw']}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classnames: cx} = this.props;
|
||||
|
||||
return (
|
||||
<div className={cx('CBItem')}>
|
||||
<a className={cx('CBItem-dragbar')}>
|
||||
<Icon icon="drag-bar" className="icon" />
|
||||
</a>
|
||||
|
||||
<div className={cx('CBItem-itemBody')}>
|
||||
{this.renderLeft()}
|
||||
{this.renderOperator()}
|
||||
{this.renderItem()}
|
||||
</div>
|
||||
{this.renderLeft()}
|
||||
{this.renderOperator()}
|
||||
{this.renderRight()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(ConditionItem);
|
||||
|
|
|
@ -1,7 +1,31 @@
|
|||
import React from 'react';
|
||||
import {FieldSimple} from './types';
|
||||
import {ThemeProps, themeable} from '../../theme';
|
||||
import InputBox from '../InputBox';
|
||||
|
||||
export default class Value extends React.Component<any> {
|
||||
export interface ValueProps extends ThemeProps {
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
field: FieldSimple;
|
||||
}
|
||||
|
||||
export class Value extends React.Component<ValueProps> {
|
||||
render() {
|
||||
return <p>Value</p>;
|
||||
const {classnames: cx, field, value, onChange} = this.props;
|
||||
let input: JSX.Element | undefined = undefined;
|
||||
|
||||
if (field.type === 'text') {
|
||||
input = (
|
||||
<InputBox
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={cx('CBValue')}>{input}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(Value);
|
||||
|
|
|
@ -18,15 +18,16 @@ export const OperationMap = {
|
|||
not_equal: '不等于',
|
||||
is_empty: '为空',
|
||||
is_not_empty: '不为空',
|
||||
like: 'LIKE',
|
||||
not_like: 'NOT LIKE',
|
||||
starts_with: 'Start With',
|
||||
ends_with: 'Ends With'
|
||||
like: '模糊匹配',
|
||||
not_like: '不匹配',
|
||||
starts_with: '匹配开头',
|
||||
ends_with: '匹配结尾'
|
||||
};
|
||||
|
||||
const defaultConfig: Config = {
|
||||
types: {
|
||||
text: {
|
||||
placeholder: '请输入文本',
|
||||
operators: [
|
||||
'equal',
|
||||
'not_equal',
|
||||
|
|
|
@ -3,8 +3,11 @@ import {ThemeProps, themeable} from '../../theme';
|
|||
import {LocaleProps, localeable} from '../../locale';
|
||||
import {uncontrollable} from 'uncontrollable';
|
||||
import {Fields, ConditionGroupValue, Funcs} from './types';
|
||||
import {ConditionGroup} from './Group';
|
||||
import ConditionGroup from './Group';
|
||||
import defaultConfig from './config';
|
||||
import {autobind, findTreeIndex} from '../../utils/helper';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import animtion from '../../utils/Animation';
|
||||
|
||||
export interface QueryBuilderProps extends ThemeProps, LocaleProps {
|
||||
fields: Fields;
|
||||
|
@ -16,6 +19,148 @@ export interface QueryBuilderProps extends ThemeProps, LocaleProps {
|
|||
export class QueryBuilder extends React.Component<QueryBuilderProps> {
|
||||
config = defaultConfig;
|
||||
|
||||
dragTarget: HTMLElement;
|
||||
ghost: HTMLElement;
|
||||
host: HTMLElement;
|
||||
lastX: number;
|
||||
lastY: number;
|
||||
lastMoveAt: number = 0;
|
||||
|
||||
@autobind
|
||||
handleDragStart(e: React.DragEvent) {
|
||||
const target = e.currentTarget;
|
||||
const item = target.closest('[data-id]') as HTMLElement;
|
||||
this.dragTarget = item;
|
||||
this.host = item.closest('[data-group-id]') as HTMLElement;
|
||||
|
||||
const ghost = item.cloneNode(true) as HTMLElement;
|
||||
ghost.classList.add('is-ghost');
|
||||
this.ghost = ghost;
|
||||
|
||||
e.dataTransfer.setDragImage(item.firstChild as HTMLElement, 0, 0);
|
||||
|
||||
const dom = findDOMNode(this) as HTMLElement;
|
||||
|
||||
target.addEventListener('dragend', this.handleDragEnd);
|
||||
dom.addEventListener('dragover', this.handleDragOver);
|
||||
dom.addEventListener('drop', this.handleDragDrop);
|
||||
this.lastX = e.clientX;
|
||||
this.lastY = e.clientY;
|
||||
|
||||
// 应该是 chrome 的一个bug,如果你马上修改,会马上执行 dragend
|
||||
setTimeout(() => {
|
||||
item.classList.add('is-dragging');
|
||||
item.parentElement!.insertBefore(
|
||||
item,
|
||||
item.parentElement!.firstElementChild
|
||||
); // 挪到第一个,主要是因为样式问题。
|
||||
}, 5);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleDragOver(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
const item = (e.target as HTMLElement).closest('[data-id]') as HTMLElement;
|
||||
|
||||
const dx = e.clientX - this.lastX;
|
||||
const dy = e.clientY - this.lastY;
|
||||
const d = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
const now = Date.now();
|
||||
|
||||
// 没移动还是不要处理,免得晃动个不停。
|
||||
if (d < 5) {
|
||||
if (this.lastMoveAt === 0) {
|
||||
} else if (now - this.lastMoveAt > 500) {
|
||||
const host = (e.target as HTMLElement).closest(
|
||||
'[data-group-id]'
|
||||
) as HTMLElement;
|
||||
|
||||
if (host) {
|
||||
this.host = host;
|
||||
this.lastMoveAt = now;
|
||||
this.lastX = 0;
|
||||
this.lastY = 0;
|
||||
this.handleDragOver(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastMoveAt = now;
|
||||
this.lastX = e.clientX;
|
||||
this.lastY = e.clientY;
|
||||
|
||||
if (
|
||||
!item ||
|
||||
item.classList.contains('is-ghost') ||
|
||||
item.closest('[data-group-id]') !== this.host
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = item.parentElement!;
|
||||
const children = [].slice.apply(container!.children);
|
||||
|
||||
const idx = children.indexOf(item);
|
||||
|
||||
if (this.ghost.parentElement !== container) {
|
||||
container.appendChild(this.ghost);
|
||||
}
|
||||
|
||||
const rect = item.getBoundingClientRect();
|
||||
const isAfter = dy > 0 && e.clientY > rect.top + rect.height / 2;
|
||||
const gIdx = isAfter ? idx : idx - 1;
|
||||
const cgIdx = children.indexOf(this.ghost);
|
||||
|
||||
if (gIdx !== cgIdx) {
|
||||
animtion.capture(container);
|
||||
|
||||
if (gIdx === children.length - 1) {
|
||||
container.appendChild(this.ghost);
|
||||
} else {
|
||||
container.insertBefore(this.ghost, children[gIdx + 1]);
|
||||
}
|
||||
|
||||
animtion.animateAll();
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleDragDrop() {
|
||||
const fromId = this.dragTarget.getAttribute('data-id')!;
|
||||
const toId = this.host.getAttribute('data-group-id')!;
|
||||
const toIndex =
|
||||
[].slice.call(this.ghost.parentElement!.children).indexOf(this.ghost) - 1;
|
||||
const value = this.props.value!;
|
||||
|
||||
const indexes = findTreeIndex([value], item => item.id === fromId);
|
||||
|
||||
if (indexes) {
|
||||
// model.setOptions(
|
||||
// spliceTree(model.options, indexes, 1, {
|
||||
// ...origin,
|
||||
// ...result
|
||||
// })
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleDragEnd(e: Event) {
|
||||
const dom = findDOMNode(this) as HTMLElement;
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
target.removeEventListener('dragend', this.handleDragEnd);
|
||||
dom.removeEventListener('dragover', this.handleDragOver);
|
||||
dom.removeEventListener('drop', this.handleDragDrop);
|
||||
|
||||
this.dragTarget.classList.remove('is-dragging');
|
||||
delete this.dragTarget;
|
||||
this.ghost.parentElement?.removeChild(this.ghost);
|
||||
delete this.ghost;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classnames: cx, fields, funcs, onChange, value} = this.props;
|
||||
|
||||
|
@ -28,6 +173,7 @@ export class QueryBuilder extends React.Component<QueryBuilderProps> {
|
|||
onChange={onChange}
|
||||
classnames={cx}
|
||||
removeable={false}
|
||||
onDragStart={this.handleDragStart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,10 +8,14 @@ export type FieldTypes =
|
|||
| 'select';
|
||||
|
||||
export type OperatorType =
|
||||
| 'equals'
|
||||
| 'not_equals'
|
||||
| 'less_than'
|
||||
| 'less_than_or_equals';
|
||||
| 'equal'
|
||||
| 'not_equal'
|
||||
| 'is_empty'
|
||||
| 'is_not_empty'
|
||||
| 'like'
|
||||
| 'not_like'
|
||||
| 'starts_with'
|
||||
| 'ends_with';
|
||||
|
||||
export type FieldItem = {
|
||||
type: 'text';
|
||||
|
@ -64,7 +68,7 @@ export interface ConditionValue extends ConditionGroupValue {}
|
|||
interface BaseField {
|
||||
type: FieldTypes;
|
||||
label: string;
|
||||
valueTypes?: Array<'value' | 'field' | 'func' | 'expression'>;
|
||||
valueTypes?: Array<'value' | 'field' | 'func' | 'raw'>;
|
||||
operators?: Array<string>;
|
||||
|
||||
// valueTypes 里面配置 func 才有效。
|
||||
|
@ -81,6 +85,7 @@ type FieldGroup = {
|
|||
interface TextField extends BaseField {
|
||||
name: string;
|
||||
type: 'text';
|
||||
placeholder?: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
@ -160,4 +165,5 @@ export type Fields = Array<Field>;
|
|||
|
||||
export type Type = {
|
||||
operators: Array<string>;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,379 @@
|
|||
// 基本上都是从 sortable 那抄的,让拖拽看起来更流畅。
|
||||
|
||||
interface Rect {
|
||||
top: number;
|
||||
left: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface AnimationState {
|
||||
rect: Rect;
|
||||
target: HTMLElement;
|
||||
}
|
||||
|
||||
function userAgent(pattern: RegExp): any {
|
||||
if (typeof window !== 'undefined' && window.navigator) {
|
||||
return !!(/*@__PURE__*/ navigator.userAgent.match(pattern));
|
||||
}
|
||||
}
|
||||
|
||||
const IE11OrLess = userAgent(
|
||||
/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i
|
||||
);
|
||||
// const Edge = userAgent(/Edge/i);
|
||||
// const FireFox = userAgent(/firefox/i);
|
||||
// const Safari =
|
||||
// userAgent(/safari/i) && !userAgent(/chrome/i) && !userAgent(/android/i);
|
||||
// const IOS = userAgent(/iP(ad|od|hone)/i);
|
||||
// const ChromeForAndroid = userAgent(/chrome/i) && userAgent(/android/i);
|
||||
|
||||
const AnimationDurtation = 150;
|
||||
const AnimationEasing = 'cubic-bezier(1, 0, 0, 1)';
|
||||
|
||||
export class AnimationManager {
|
||||
animating: boolean = false;
|
||||
animationCallbackId: any;
|
||||
states: Array<AnimationState> = [];
|
||||
|
||||
capture(el: HTMLElement) {
|
||||
// 清空,重复调用,旧的不管了
|
||||
this.states = [];
|
||||
|
||||
let children: Array<HTMLElement> = [].slice.call(el.children);
|
||||
children.forEach(child => {
|
||||
// 如果是 ghost
|
||||
if (child.classList.contains('is-ghost')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = getRect(child)!;
|
||||
|
||||
// 通常是隐藏节点
|
||||
if (!rect.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fromRect = {...rect};
|
||||
|
||||
const state: AnimationState = {
|
||||
target: child,
|
||||
rect
|
||||
};
|
||||
|
||||
// 还在动画中
|
||||
if ((child as any).thisAnimationDuration) {
|
||||
let childMatrix = matrix(child);
|
||||
|
||||
if (childMatrix) {
|
||||
fromRect.top -= childMatrix.f;
|
||||
fromRect.left -= childMatrix.e;
|
||||
}
|
||||
}
|
||||
|
||||
(child as any).fromRect = fromRect;
|
||||
this.states.push(state);
|
||||
});
|
||||
}
|
||||
|
||||
animateAll(callback?: () => void) {
|
||||
this.animating = false;
|
||||
let animationTime = 0;
|
||||
|
||||
this.states.forEach(state => {
|
||||
let time = 0,
|
||||
target = state.target,
|
||||
fromRect = (target as any).fromRect,
|
||||
toRect = {
|
||||
...getRect(target)!
|
||||
},
|
||||
prevFromRect = (target as any).prevFromRect,
|
||||
prevToRect = (target as any).prevToRect,
|
||||
animatingRect = state.rect,
|
||||
targetMatrix = matrix(target);
|
||||
|
||||
if (targetMatrix) {
|
||||
// Compensate for current animation
|
||||
toRect.top -= targetMatrix.f;
|
||||
toRect.left -= targetMatrix.e;
|
||||
}
|
||||
|
||||
(target as any).toRect = toRect;
|
||||
|
||||
if ((target as any).thisAnimationDuration) {
|
||||
// Could also check if animatingRect is between fromRect and toRect
|
||||
if (
|
||||
isRectEqual(prevFromRect, toRect) &&
|
||||
!isRectEqual(fromRect, toRect) &&
|
||||
// Make sure animatingRect is on line between toRect & fromRect
|
||||
(animatingRect.top - toRect.top) /
|
||||
(animatingRect.left - toRect.left) ===
|
||||
(fromRect.top - toRect.top) / (fromRect.left - toRect.left)
|
||||
) {
|
||||
// If returning to same place as started from animation and on same axis
|
||||
time = calculateRealTime(animatingRect, prevFromRect, prevToRect);
|
||||
}
|
||||
}
|
||||
|
||||
// if fromRect != toRect: animate
|
||||
if (!isRectEqual(toRect, fromRect)) {
|
||||
(target as any).prevFromRect = fromRect;
|
||||
(target as any).prevToRect = toRect;
|
||||
|
||||
if (!time) {
|
||||
time = AnimationDurtation;
|
||||
}
|
||||
this.animate(target, animatingRect, toRect, time);
|
||||
}
|
||||
|
||||
if (time) {
|
||||
this.animating = true;
|
||||
animationTime = Math.max(animationTime, time);
|
||||
clearTimeout((target as any).animationResetTimer);
|
||||
(target as any).animationResetTimer = setTimeout(function () {
|
||||
(target as any).animationTime = 0;
|
||||
(target as any).prevFromRect = null;
|
||||
(target as any).fromRect = null;
|
||||
(target as any).prevToRect = null;
|
||||
(target as any).thisAnimationDuration = null;
|
||||
}, time);
|
||||
(target as any).thisAnimationDuration = time;
|
||||
}
|
||||
});
|
||||
|
||||
clearTimeout(this.animationCallbackId);
|
||||
if (!this.animating) {
|
||||
if (typeof callback === 'function') callback();
|
||||
} else {
|
||||
this.animationCallbackId = setTimeout(() => {
|
||||
this.animating = false;
|
||||
if (typeof callback === 'function') callback();
|
||||
}, animationTime);
|
||||
}
|
||||
this.states = [];
|
||||
}
|
||||
|
||||
animate(
|
||||
target: HTMLElement,
|
||||
currentRect: Rect,
|
||||
toRect: Rect,
|
||||
duration: number
|
||||
) {
|
||||
if (duration) {
|
||||
let affectDisplay = false;
|
||||
css(target, 'transition', '');
|
||||
css(target, 'transform', '');
|
||||
let translateX = currentRect.left - toRect.left,
|
||||
translateY = currentRect.top - toRect.top;
|
||||
|
||||
(target as any).animatingX = !!translateX;
|
||||
(target as any).animatingY = !!translateY;
|
||||
|
||||
css(
|
||||
target,
|
||||
'transform',
|
||||
'translate3d(' + translateX + 'px,' + translateY + 'px,0)'
|
||||
);
|
||||
|
||||
if (css(target, 'display') === 'inline') {
|
||||
affectDisplay = true;
|
||||
css(target, 'display', 'inline-block');
|
||||
}
|
||||
|
||||
target.offsetWidth; // repaint
|
||||
|
||||
css(
|
||||
target,
|
||||
'transition',
|
||||
'transform ' +
|
||||
duration +
|
||||
'ms' +
|
||||
(AnimationEasing ? ' ' + AnimationEasing : '')
|
||||
);
|
||||
css(target, 'transform', 'translate3d(0,0,0)');
|
||||
typeof (target as any).animated === 'number' &&
|
||||
clearTimeout((target as any).animated);
|
||||
(target as any).animated = setTimeout(function () {
|
||||
css(target, 'transition', '');
|
||||
css(target, 'transform', '');
|
||||
affectDisplay && css(target, 'display', '');
|
||||
(target as any).animated = false;
|
||||
|
||||
(target as any).animatingX = false;
|
||||
(target as any).animatingY = false;
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function matrix(el: HTMLElement) {
|
||||
let appliedTransforms = '';
|
||||
if (typeof el === 'string') {
|
||||
appliedTransforms = el;
|
||||
} else {
|
||||
let transform = css(el, 'transform');
|
||||
|
||||
if (transform && transform !== 'none') {
|
||||
appliedTransforms = transform + ' ' + appliedTransforms;
|
||||
}
|
||||
}
|
||||
|
||||
const matrixFn =
|
||||
window.DOMMatrix ||
|
||||
window.WebKitCSSMatrix ||
|
||||
(window as any).CSSMatrix ||
|
||||
(window as any).MSCSSMatrix;
|
||||
|
||||
/*jshint -W056 */
|
||||
return matrixFn && new matrixFn(appliedTransforms);
|
||||
}
|
||||
|
||||
function css(el: HTMLElement, prop: string, val?: any) {
|
||||
let style = el && el.style;
|
||||
|
||||
if (style) {
|
||||
if (val === void 0) {
|
||||
if (document.defaultView && document.defaultView.getComputedStyle) {
|
||||
val = document.defaultView.getComputedStyle(el, '');
|
||||
} else if ((el as any).currentStyle) {
|
||||
val = (el as any).currentStyle;
|
||||
}
|
||||
|
||||
return prop === void 0 ? val : val[prop];
|
||||
} else {
|
||||
if (!(prop in style) && prop.indexOf('webkit') === -1) {
|
||||
prop = '-webkit-' + prop;
|
||||
}
|
||||
|
||||
(style as any)[prop] = val + (typeof val === 'string' ? '' : 'px');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isRectEqual(rect1: Rect, rect2: Rect) {
|
||||
return (
|
||||
Math.round(rect1.top) === Math.round(rect2.top) &&
|
||||
Math.round(rect1.left) === Math.round(rect2.left) &&
|
||||
Math.round(rect1.height) === Math.round(rect2.height) &&
|
||||
Math.round(rect1.width) === Math.round(rect2.width)
|
||||
);
|
||||
}
|
||||
|
||||
function calculateRealTime(animatingRect: Rect, fromRect: Rect, toRect: Rect) {
|
||||
return (
|
||||
(Math.sqrt(
|
||||
Math.pow(fromRect.top - animatingRect.top, 2) +
|
||||
Math.pow(fromRect.left - animatingRect.left, 2)
|
||||
) /
|
||||
Math.sqrt(
|
||||
Math.pow(fromRect.top - toRect.top, 2) +
|
||||
Math.pow(fromRect.left - toRect.left, 2)
|
||||
)) *
|
||||
AnimationDurtation
|
||||
);
|
||||
}
|
||||
|
||||
function getWindowScrollingElement() {
|
||||
let scrollingElement = document.scrollingElement;
|
||||
|
||||
if (scrollingElement) {
|
||||
return scrollingElement;
|
||||
} else {
|
||||
return document.documentElement;
|
||||
}
|
||||
}
|
||||
|
||||
function getRect(
|
||||
el: HTMLElement,
|
||||
relativeToContainingBlock?: boolean,
|
||||
relativeToNonStaticParent?: boolean,
|
||||
undoScale?: boolean,
|
||||
container?: HTMLElement
|
||||
) {
|
||||
if (!el.getBoundingClientRect && (el as any) !== window) return;
|
||||
|
||||
let elRect, top, left, bottom, right, height, width;
|
||||
|
||||
if ((el as any) !== window && el !== getWindowScrollingElement()) {
|
||||
elRect = el.getBoundingClientRect();
|
||||
top = elRect.top;
|
||||
left = elRect.left;
|
||||
bottom = elRect.bottom;
|
||||
right = elRect.right;
|
||||
height = elRect.height;
|
||||
width = elRect.width;
|
||||
} else {
|
||||
top = 0;
|
||||
left = 0;
|
||||
bottom = window.innerHeight;
|
||||
right = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
width = window.innerWidth;
|
||||
}
|
||||
|
||||
if (
|
||||
(relativeToContainingBlock || relativeToNonStaticParent) &&
|
||||
(el as any) !== window
|
||||
) {
|
||||
// Adjust for translate()
|
||||
container = container || (el.parentNode as HTMLElement);
|
||||
|
||||
// solves #1123 (see: https://stackoverflow.com/a/37953806/6088312)
|
||||
// Not needed on <= IE11
|
||||
if (!IE11OrLess) {
|
||||
do {
|
||||
if (
|
||||
container &&
|
||||
container.getBoundingClientRect &&
|
||||
(css(container, 'transform') !== 'none' ||
|
||||
(relativeToNonStaticParent &&
|
||||
css(container, 'position') !== 'static'))
|
||||
) {
|
||||
let containerRect = container.getBoundingClientRect();
|
||||
|
||||
// Set relative to edges of padding box of container
|
||||
top -=
|
||||
containerRect.top + parseInt(css(container, 'border-top-width'));
|
||||
left -=
|
||||
containerRect.left + parseInt(css(container, 'border-left-width'));
|
||||
bottom = top + (elRect as any).height;
|
||||
right = left + (elRect as any).width;
|
||||
|
||||
break;
|
||||
}
|
||||
/* jshint boss:true */
|
||||
} while ((container = container.parentNode as HTMLElement));
|
||||
}
|
||||
}
|
||||
|
||||
if (undoScale && (el as any) !== window) {
|
||||
// Adjust for scale()
|
||||
let elMatrix = matrix(container || el),
|
||||
scaleX = elMatrix && elMatrix.a,
|
||||
scaleY = elMatrix && elMatrix.d;
|
||||
|
||||
if (elMatrix) {
|
||||
top /= scaleY;
|
||||
left /= scaleX;
|
||||
|
||||
width /= scaleX;
|
||||
height /= scaleY;
|
||||
|
||||
bottom = top + height;
|
||||
right = left + width;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
top: top,
|
||||
left: left,
|
||||
bottom: bottom,
|
||||
right: right,
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}
|
||||
|
||||
export default new AnimationManager();
|
Loading…
Reference in New Issue