diff --git a/scss/components/_condition-builder.scss b/scss/components/_condition-builder.scss index 7ae097e6..14719e18 100644 --- a/scss/components/_condition-builder.scss +++ b/scss/components/_condition-builder.scss @@ -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; +} diff --git a/src/components/condition-builder/Expression.tsx b/src/components/condition-builder/Expression.tsx index d51f6158..99c6f349 100644 --- a/src/components/condition-builder/Expression.tsx +++ b/src/components/condition-builder/Expression.tsx @@ -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 { 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 { /> ) : null} - {inputType === 'value' ? : null} + {inputType === 'value' ? ( + + ) : null} {inputType === 'field' ? ( 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 { getValue() { return { + id: guid(), conjunction: 'and' as 'and', ...this.props.value } as ConditionGroupValue; @@ -31,7 +33,7 @@ export class ConditionGroup extends React.Component { 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 { 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 { value.children.push({ id: guid() }); - onChange(value, this.props.index); + onChange(value); } @autobind @@ -68,13 +70,18 @@ export class ConditionGroup extends React.Component { 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 { : []; 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 ( -
+
- - + {removeable ? ( + + + + ) : null}
- {Array.isArray(value?.children) - ? value!.children.map((item, index) => - (item as ConditionGroupValue).conjunction ? ( - + {Array.isArray(value?.children) + ? value!.children.map((item, index) => ( + 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} /> - ) : ( - - ) - ) - : null} + )) + : null} +
); } } + +export default themeable(ConditionGroup); diff --git a/src/components/condition-builder/GroupOrItem.tsx b/src/components/condition-builder/GroupOrItem.tsx new file mode 100644 index 00000000..76dd280b --- /dev/null +++ b/src/components/condition-builder/GroupOrItem.tsx @@ -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 { + @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 ( +
+
+ {draggable ? ( + + + + ) : null} + + {value?.conjunction ? ( + + ) : ( + <> + + + + + + )} +
+
+ ); + } +} + +export default themeable(CBGroupOrItem); diff --git a/src/components/condition-builder/Item.tsx b/src/components/condition-builder/Item.tsx index a9ffdc91..ad6e6565 100644 --- a/src/components/condition-builder/Item.tsx +++ b/src/components/condition-builder/Item.tsx @@ -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 { } @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 { ) 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 { 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 ( + + ); + } + render() { const {classnames: cx} = this.props; return (
- - - - -
- {this.renderLeft()} - {this.renderOperator()} - {this.renderItem()} -
+ {this.renderLeft()} + {this.renderOperator()} + {this.renderRight()}
); } } + +export default themeable(ConditionItem); diff --git a/src/components/condition-builder/Value.tsx b/src/components/condition-builder/Value.tsx index 1f29824f..a3471570 100644 --- a/src/components/condition-builder/Value.tsx +++ b/src/components/condition-builder/Value.tsx @@ -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 { +export interface ValueProps extends ThemeProps { + value: any; + onChange: (value: any) => void; + field: FieldSimple; +} + +export class Value extends React.Component { render() { - return

Value

; + const {classnames: cx, field, value, onChange} = this.props; + let input: JSX.Element | undefined = undefined; + + if (field.type === 'text') { + input = ( + + ); + } + + return
{input}
; } } + +export default themeable(Value); diff --git a/src/components/condition-builder/config.ts b/src/components/condition-builder/config.ts index a0353821..b212c31f 100644 --- a/src/components/condition-builder/config.ts +++ b/src/components/condition-builder/config.ts @@ -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', diff --git a/src/components/condition-builder/index.tsx b/src/components/condition-builder/index.tsx index 7ae55f55..5db8dcb5 100644 --- a/src/components/condition-builder/index.tsx +++ b/src/components/condition-builder/index.tsx @@ -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 { 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 { onChange={onChange} classnames={cx} removeable={false} + onDragStart={this.handleDragStart} /> ); } diff --git a/src/components/condition-builder/types.ts b/src/components/condition-builder/types.ts index 1c72a5ed..cf1e9824 100644 --- a/src/components/condition-builder/types.ts +++ b/src/components/condition-builder/types.ts @@ -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; // 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; export type Type = { operators: Array; + placeholder?: string; }; diff --git a/src/utils/Animation.ts b/src/utils/Animation.ts new file mode 100644 index 00000000..c376588d --- /dev/null +++ b/src/utils/Animation.ts @@ -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 = []; + + capture(el: HTMLElement) { + // 清空,重复调用,旧的不管了 + this.states = []; + + let children: Array = [].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();