条件组合半成品

This commit is contained in:
2betop 2020-08-17 20:32:07 +08:00
parent 8901b57389
commit 9bb718aeac
10 changed files with 888 additions and 79 deletions

View File

@ -38,17 +38,43 @@
} }
} }
.#{$ns}CBItem { .#{$ns}CBDelete {
display: flex; @include icon-color();
margin-top: px2rem(10px); cursor: pointer;
padding: 5px 10px; margin-left: auto;
border-radius: 5px; }
flex-direction: row;
align-items: center; .#{$ns}CBGroupOrItem {
margin-left: px2rem(30px);
position: relative; position: relative;
background: rgba(0, 0, 0, 0.03); margin-left: px2rem(30px);
transition: all 0.3s ease-out;
&-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 { &-dragbar {
cursor: move; cursor: move;
@ -60,11 +86,16 @@
@include icon-color(); @include icon-color();
} }
.#{$ns}CBGroup {
margin-left: $gap-xs;
flex-grow: 1;
}
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
&:hover &-dragbar { &:hover > &-body > &-dragbar {
opacity: 1; opacity: 1;
} }
@ -117,6 +148,7 @@
.#{$ns}CBFunc { .#{$ns}CBFunc {
display: inline-block; display: inline-block;
margin-left: $gap-xs;
&-select { &-select {
display: inline-block; display: inline-block;
@ -140,3 +172,9 @@
} }
} }
} }
.#{$ns}CBValue {
position: relative;
display: inline-block;
margin-left: $gap-xs;
}

View File

@ -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 React from 'react';
import ConditionField from './Field'; import ConditionField from './Field';
import {autobind, findTree} from '../../utils/helper'; import {autobind, findTree} from '../../utils/helper';
@ -20,7 +28,7 @@ export interface ExpressionProps extends ThemeProps {
value: ExpressionComplex; value: ExpressionComplex;
index?: number; index?: number;
onChange: (value: ExpressionComplex, index?: number) => void; onChange: (value: ExpressionComplex, index?: number) => void;
valueField?: Field; valueField?: FieldSimple;
fields?: Field[]; fields?: Field[];
funcs?: Funcs; funcs?: Funcs;
defaultType?: 'value' | 'field' | 'func' | 'raw'; defaultType?: 'value' | 'field' | 'func' | 'raw';
@ -94,7 +102,14 @@ export class Expression extends React.Component<ExpressionProps> {
handleRawChange() {} handleRawChange() {}
render() { render() {
const {value, defaultType, allowedTypes, funcs, fields} = this.props; const {
value,
valueField,
defaultType,
allowedTypes,
funcs,
fields
} = this.props;
const inputType = const inputType =
((value as any)?.type === 'field' ((value as any)?.type === 'field'
? 'field' ? 'field'
@ -128,7 +143,13 @@ export class Expression extends React.Component<ExpressionProps> {
/> />
) : null} ) : null}
{inputType === 'value' ? <Value /> : null} {inputType === 'value' ? (
<Value
field={valueField!}
value={value}
onChange={this.handleValueChange}
/>
) : null}
{inputType === 'field' ? ( {inputType === 'field' ? (
<ConditionField <ConditionField

View File

@ -1,25 +1,27 @@
import React from 'react'; import React from 'react';
import {Fields, ConditionGroupValue, Funcs} from './types'; import {Fields, ConditionGroupValue, Funcs} from './types';
import {ClassNamesFn} from '../../theme'; import {ClassNamesFn, ThemeProps, themeable} from '../../theme';
import Button from '../Button'; import Button from '../Button';
import {ConditionItem} from './Item'; import GroupOrItem from './GroupOrItem';
import {autobind, guid} from '../../utils/helper'; import {autobind, guid} from '../../utils/helper';
import {Config} from './config'; import {Config} from './config';
import {Icon} from '../icons';
export interface ConditionGroupProps { export interface ConditionGroupProps extends ThemeProps {
config: Config; config: Config;
value?: ConditionGroupValue; value?: ConditionGroupValue;
fields: Fields; fields: Fields;
funcs?: Funcs; funcs?: Funcs;
index?: number; onChange: (value: ConditionGroupValue) => void;
onChange: (value: ConditionGroupValue, index?: number) => void;
classnames: ClassNamesFn;
removeable?: boolean; removeable?: boolean;
onRemove?: (e: React.MouseEvent) => void;
onDragStart?: (e: React.MouseEvent) => void;
} }
export class ConditionGroup extends React.Component<ConditionGroupProps> { export class ConditionGroup extends React.Component<ConditionGroupProps> {
getValue() { getValue() {
return { return {
id: guid(),
conjunction: 'and' as 'and', conjunction: 'and' as 'and',
...this.props.value ...this.props.value
} as ConditionGroupValue; } as ConditionGroupValue;
@ -31,7 +33,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
let value = this.getValue(); let value = this.getValue();
value.not = !value.not; value.not = !value.not;
onChange(value, this.props.index); onChange(value);
} }
@autobind @autobind
@ -39,7 +41,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
const onChange = this.props.onChange; const onChange = this.props.onChange;
let value = this.getValue(); let value = this.getValue();
value.conjunction = value.conjunction === 'and' ? 'or' : 'and'; value.conjunction = value.conjunction === 'and' ? 'or' : 'and';
onChange(value, this.props.index); onChange(value);
} }
@autobind @autobind
@ -54,7 +56,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
value.children.push({ value.children.push({
id: guid() id: guid()
}); });
onChange(value, this.props.index); onChange(value);
} }
@autobind @autobind
@ -68,13 +70,18 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
value.children.push({ value.children.push({
id: guid(), id: guid(),
conjunction: 'and' conjunction: 'and',
children: [
{
id: guid()
}
]
}); });
onChange(value, this.props.index); onChange(value);
} }
@autobind @autobind
handleItemChange(item: any, index?: number) { handleItemChange(item: any, index: number) {
const onChange = this.props.onChange; const onChange = this.props.onChange;
let value = this.getValue(); let value = this.getValue();
@ -83,14 +90,36 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
: []; : [];
value.children.splice(index!, 1, item); 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() { render() {
const {classnames: cx, value, fields, funcs, config} = this.props; const {
classnames: cx,
value,
fields,
funcs,
config,
removeable,
onRemove,
onDragStart
} = this.props;
return ( return (
<div className={cx('CBGroup')}> <div className={cx('CBGroup')} data-group-id={value?.id}>
<div className={cx('CBGroup-toolbar')}> <div className={cx('CBGroup-toolbar')}>
<div className={cx('CBGroup-toolbarLeft')}> <div className={cx('CBGroup-toolbarLeft')}>
<Button onClick={this.handleNotClick} size="sm" active={value?.not}> <Button onClick={this.handleNotClick} size="sm" active={value?.not}>
@ -114,43 +143,41 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
</div> </div>
</div> </div>
<div className={cx('CBGroup-toolbarRight')}> <div className={cx('CBGroup-toolbarRight')}>
<Button onClick={this.handleAdd} size="sm"> <Button onClick={this.handleAdd} size="sm" className="m-r-xs">
</Button> </Button>
<Button onClick={this.handleAddGroup} size="sm" className="m-l-xs"> <Button onClick={this.handleAddGroup} size="sm" className="m-r-xs">
</Button> </Button>
{removeable ? (
<a className={cx('CBDelete')} onClick={onRemove}>
<Icon icon="close" className="icon" />
</a>
) : null}
</div> </div>
</div> </div>
{Array.isArray(value?.children) <div className={cx('CBGroup-body')}>
? value!.children.map((item, index) => {Array.isArray(value?.children)
(item as ConditionGroupValue).conjunction ? ( ? value!.children.map((item, index) => (
<ConditionGroup <GroupOrItem
draggable={value!.children!.length > 1}
onDragStart={onDragStart}
config={config} config={config}
key={item.id} key={item.id}
fields={fields} fields={fields}
value={item as ConditionGroupValue} value={item as ConditionGroupValue}
classnames={cx}
index={index} index={index}
onChange={this.handleItemChange} onChange={this.handleItemChange}
funcs={funcs} funcs={funcs}
onRemove={this.handleItemRemove}
/> />
) : ( ))
<ConditionItem : null}
config={config} </div>
key={item.id}
fields={fields}
value={item}
classnames={cx}
index={index}
onChange={this.handleItemChange}
funcs={funcs}
/>
)
)
: null}
</div> </div>
); );
} }
} }
export default themeable(ConditionGroup);

View File

@ -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);

View File

@ -8,9 +8,10 @@ import {
Func, Func,
Field, Field,
FieldSimple, FieldSimple,
ExpressionField ExpressionField,
OperatorType
} from './types'; } from './types';
import {ClassNamesFn} from '../../theme'; import {ThemeProps, themeable} from '../../theme';
import {Icon} from '../icons'; import {Icon} from '../icons';
import {autobind, findTree, noop} from '../../utils/helper'; import {autobind, findTree, noop} from '../../utils/helper';
import Expression from './Expression'; import Expression from './Expression';
@ -21,13 +22,12 @@ import ResultBox from '../ResultBox';
const option2value = (item: any) => item.value; const option2value = (item: any) => item.value;
export interface ConditionItemProps { export interface ConditionItemProps extends ThemeProps {
config: Config; config: Config;
fields: Fields; fields: Fields;
funcs?: Funcs; funcs?: Funcs;
index?: number; index?: number;
value: ConditionRule; value: ConditionRule;
classnames: ClassNamesFn;
onChange: (value: ConditionRule, index?: number) => void; onChange: (value: ConditionRule, index?: number) => void;
} }
@ -63,7 +63,18 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
} }
@autobind @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() { renderLeft() {
const {value, fields, funcs} = this.props; const {value, fields, funcs} = this.props;
@ -101,7 +112,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
) as FieldSimple; ) as FieldSimple;
if (field) { 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; 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; 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() { render() {
const {classnames: cx} = this.props; const {classnames: cx} = this.props;
return ( return (
<div className={cx('CBItem')}> <div className={cx('CBItem')}>
<a className={cx('CBItem-dragbar')}> {this.renderLeft()}
<Icon icon="drag-bar" className="icon" /> {this.renderOperator()}
</a> {this.renderRight()}
<div className={cx('CBItem-itemBody')}>
{this.renderLeft()}
{this.renderOperator()}
{this.renderItem()}
</div>
</div> </div>
); );
} }
} }
export default themeable(ConditionItem);

View File

@ -1,7 +1,31 @@
import React from 'react'; 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() { 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);

View File

@ -18,15 +18,16 @@ export const OperationMap = {
not_equal: '不等于', not_equal: '不等于',
is_empty: '为空', is_empty: '为空',
is_not_empty: '不为空', is_not_empty: '不为空',
like: 'LIKE', like: '模糊匹配',
not_like: 'NOT LIKE', not_like: '不匹配',
starts_with: 'Start With', starts_with: '匹配开头',
ends_with: 'Ends With' ends_with: '匹配结尾'
}; };
const defaultConfig: Config = { const defaultConfig: Config = {
types: { types: {
text: { text: {
placeholder: '请输入文本',
operators: [ operators: [
'equal', 'equal',
'not_equal', 'not_equal',

View File

@ -3,8 +3,11 @@ import {ThemeProps, themeable} from '../../theme';
import {LocaleProps, localeable} from '../../locale'; import {LocaleProps, localeable} from '../../locale';
import {uncontrollable} from 'uncontrollable'; import {uncontrollable} from 'uncontrollable';
import {Fields, ConditionGroupValue, Funcs} from './types'; import {Fields, ConditionGroupValue, Funcs} from './types';
import {ConditionGroup} from './Group'; import ConditionGroup from './Group';
import defaultConfig from './config'; 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 { export interface QueryBuilderProps extends ThemeProps, LocaleProps {
fields: Fields; fields: Fields;
@ -16,6 +19,148 @@ export interface QueryBuilderProps extends ThemeProps, LocaleProps {
export class QueryBuilder extends React.Component<QueryBuilderProps> { export class QueryBuilder extends React.Component<QueryBuilderProps> {
config = defaultConfig; 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() { render() {
const {classnames: cx, fields, funcs, onChange, value} = this.props; const {classnames: cx, fields, funcs, onChange, value} = this.props;
@ -28,6 +173,7 @@ export class QueryBuilder extends React.Component<QueryBuilderProps> {
onChange={onChange} onChange={onChange}
classnames={cx} classnames={cx}
removeable={false} removeable={false}
onDragStart={this.handleDragStart}
/> />
); );
} }

View File

@ -8,10 +8,14 @@ export type FieldTypes =
| 'select'; | 'select';
export type OperatorType = export type OperatorType =
| 'equals' | 'equal'
| 'not_equals' | 'not_equal'
| 'less_than' | 'is_empty'
| 'less_than_or_equals'; | 'is_not_empty'
| 'like'
| 'not_like'
| 'starts_with'
| 'ends_with';
export type FieldItem = { export type FieldItem = {
type: 'text'; type: 'text';
@ -64,7 +68,7 @@ export interface ConditionValue extends ConditionGroupValue {}
interface BaseField { interface BaseField {
type: FieldTypes; type: FieldTypes;
label: string; label: string;
valueTypes?: Array<'value' | 'field' | 'func' | 'expression'>; valueTypes?: Array<'value' | 'field' | 'func' | 'raw'>;
operators?: Array<string>; operators?: Array<string>;
// valueTypes 里面配置 func 才有效。 // valueTypes 里面配置 func 才有效。
@ -81,6 +85,7 @@ type FieldGroup = {
interface TextField extends BaseField { interface TextField extends BaseField {
name: string; name: string;
type: 'text'; type: 'text';
placeholder?: string;
minLength?: number; minLength?: number;
maxLength?: number; maxLength?: number;
} }
@ -160,4 +165,5 @@ export type Fields = Array<Field>;
export type Type = { export type Type = {
operators: Array<string>; operators: Array<string>;
placeholder?: string;
}; };

379
src/utils/Animation.ts Normal file
View File

@ -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();