Merge pull request #30 from H5-Dooring/animate-patch

Animate patch
This commit is contained in:
yehuozhili
2022-04-01 14:36:41 +08:00
committed by GitHub
10 changed files with 653 additions and 156 deletions

View File

@@ -1,9 +1,22 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import { UserConfig, deepCopy, createUid } from 'dooringx-lib';
import { Col, Row, Select, InputNumber, Button } from 'antd';
import {
Col,
Row,
Select,
InputNumber,
Button,
Modal,
Form,
Space,
Input,
Table,
Popconfirm,
} from 'antd';
import { FormMap, FormBaseType } from '../formTypes';
import { CreateOptionsRes } from 'dooringx-lib/dist/core/components/formTypes';
import { AnimateItem, IBlockType, IStoreData } from 'dooringx-lib/dist/core/store/storetype';
import { PlusOutlined } from '@ant-design/icons';
export interface FormAnimateControlType extends FormBaseType {}
@@ -13,7 +26,7 @@ interface AnimateControlProps {
config: UserConfig;
}
//类型待修改
// 左侧实际类名-显示名称
const animateCategory: Record<string, string> = {
'': '无',
animate__bounce: 'bounce',
@@ -134,11 +147,75 @@ function AnimateControl(props: AnimateControlProps) {
lastAnimate = props.current.animate;
return props.current.animate;
}, [props.current.animate]);
const [customModal, setCustomModal] = useState(false);
const [form] = Form.useForm();
const [deletModal, setDeletModal] = useState(false);
const customAnimate = props.config.animateFactory.getCustomAnimateName();
const columns = [
{
title: '动画名称',
dataIndex: 'animateName',
width: 150,
},
{
title: '显示名称',
dataIndex: 'displayName',
width: 150,
},
{
title: '动画详情',
dataIndex: 'keyframe',
},
{
title: '操作',
width: 120,
render: (_: any, record: any) => (
<Space size="middle">
<Popconfirm
onConfirm={() => {
const name = record.animateName;
props.config.animateFactory.deleteCustomAnimate(name);
props.config.animateFactory.deleteKeyFrameAnimate(name);
props.config.animateFactory.syncToStore(props.config);
}}
title="确定删除吗?"
>
<a></a>
</Popconfirm>
</Space>
),
},
];
return (
<>
<Space
style={{ display: 'flex', marginBottom: 8, justifyContent: 'space-around' }}
align="baseline"
>
<Button
onClick={() => {
setCustomModal(true);
}}
>
</Button>
<Button
onClick={() => {
setDeletModal(true);
}}
>
</Button>
</Space>
{animate.map((v, i) => {
return (
<div key={v.uid} style={{ borderBottom: '1px dotted #9e9e9e' }}>
{
<>
<Row style={{ padding: padding, alignItems: 'center' }}>
<Col span={5}>:</Col>
<Col span={7}>
@@ -166,6 +243,13 @@ function AnimateControl(props: AnimateControlProps) {
</Select.Option>
);
})}
{customAnimate.map((v) => {
return (
<Select.Option key={v.animateName} value={v.animateName}>
{v.displayName}
</Select.Option>
);
})}
</Select>
</Col>
<Col span={5} style={{ paddingLeft: '10px' }}>
@@ -295,6 +379,8 @@ function AnimateControl(props: AnimateControlProps) {
</Button>
</Col>
</Row>
</>
}
</div>
);
})}
@@ -357,6 +443,150 @@ function AnimateControl(props: AnimateControlProps) {
</Button>
</Row>
<Modal
width={800}
title={'设置自定义动画'}
forceRender
visible={customModal}
onOk={() => {
form.validateFields().then((res) => {
const values = { ...res };
props.config.animateFactory.addUserInputIntoCustom(values, props.config);
setCustomModal(false);
form.resetFields();
});
}}
onCancel={() => {
setCustomModal(false);
form.resetFields();
}}
>
<Form labelCol={{ span: 11 }} form={form}>
<Form.Item
required
rules={[{ required: true, message: '请输入名称!' }]}
labelCol={{ span: 6 }}
name="displayName"
label="自定义动画显示名称"
>
<Input></Input>
</Form.Item>
<Form.Item
initialValue={`dooringx_${Math.random().toString(36).slice(2)}`}
labelCol={{ span: 6 }}
name="animateName"
label="自定义动画名称"
>
<Input disabled></Input>
</Form.Item>
<Form.List name="keyframes">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space
key={key}
style={{ display: 'flex', marginBottom: 8, flexWrap: 'wrap' }}
align="baseline"
>
<Form.Item
style={{ width: 180 }}
label="时间百分比"
{...restField}
name={[name, 'percent']}
initialValue={0}
>
<InputNumber min={0} max={100} formatter={(value) => `${value}%`} />
</Form.Item>
<>
<Form.Item
style={{ width: 180 }}
label="坐标偏移X"
{...restField}
name={[name, 'positionX']}
initialValue={0}
>
<InputNumber />
</Form.Item>
<Form.Item
style={{ width: 180 }}
label="坐标偏移Y"
{...restField}
name={[name, 'positionY']}
initialValue={0}
>
<InputNumber />
</Form.Item>
</>
<Form.Item
style={{ width: 180 }}
label="旋转"
{...restField}
name={[name, 'rotate']}
initialValue={0}
>
<InputNumber formatter={(value) => `${value}°`} />
</Form.Item>
<Form.Item
style={{ width: 180 }}
label="缩放"
{...restField}
name={[name, 'scale']}
initialValue={100}
>
<InputNumber formatter={(value) => `${value}%`} />
</Form.Item>
<Form.Item
style={{ width: 180 }}
label="透明度"
{...restField}
name={[name, 'opacity']}
initialValue={100}
>
<InputNumber formatter={(value) => `${value}%`} />
</Form.Item>
<Button
danger
onClick={() => {
remove(name);
}}
>
</Button>
</Space>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
block
icon={<PlusOutlined />}
>
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
<Modal
width={800}
title={'删除自定义动画'}
forceRender
visible={deletModal}
footer={null}
onOk={() => {
setDeletModal(false);
}}
onCancel={() => {
setDeletModal(false);
}}
>
<Table columns={columns} dataSource={customAnimate}></Table>
</Modal>
</>
);
}

View File

@@ -29,6 +29,13 @@ const LeftRegistMap: LeftRegistComponentMapItem[] = [
img: 'https://img.guguzhu.com/d/file/android/ico/2021/09/08/rytzi2w34tm.png',
displayName: '输入框',
},
{
type: 'basic',
component: 'test',
img: 'https://img.guguzhu.com/d/file/android/ico/2021/09/08/rytzi2w34tm.png',
displayName: '测试',
urlFn: () => import('./registComponents/testCo'),
},
];
export const defaultConfig: Partial<InitConfig> = {

View File

@@ -0,0 +1,17 @@
import { ComponentItemFactory } from 'dooringx-lib';
const TestCo = new ComponentItemFactory(
'test',
'测试组件',
{},
{
width: 200,
height: 55,
},
() => {
return <div></div>;
},
true
);
export default TestCo;

View File

@@ -2,7 +2,7 @@
* @Author: yehuozhili
* @Date: 2021-03-14 05:40:37
* @LastEditors: yehuozhili
* @LastEditTime: 2021-10-10 00:54:55
* @LastEditTime: 2022-04-01 14:29:39
* @FilePath: \dooringx\packages\dooringx-lib\src\components\preview.tsx
*/
import Container from './container';
@@ -62,10 +62,17 @@ function Preview(props: PreviewProps): ReactElement {
.syncEventMap(props.config.getStore().getData(), props.config.getStoreChanger());
// 设置全局
const bodyColor = props.config.getStore().getData().globalState?.bodyColor;
const global = props.config.getStore().getData().globalState;
const bodyColor = global?.bodyColor;
if (bodyColor) {
document.body.style.backgroundColor = bodyColor;
}
const customAnimate = global?.customAnimate;
if (customAnimate && Array.isArray(customAnimate)) {
// 插入自定义动画
props.config.animateFactory.fromArrInsertKeyFrame(customAnimate);
}
if (props.completeFn) {
props.completeFn();
}

View File

@@ -84,7 +84,11 @@ function RightConfig(props: PropsWithChildren<RightConfigProps>) {
);
});
} else {
return <div>{replaceLocale('right.noprops', '还没有配置属性', props.config)}</div>;
return (
<div className="yh-right-noprops" style={{ textAlign: 'center' }}>
{replaceLocale('right.noprops', '还没有配置属性', props.config)}
</div>
);
}
}
return null;

View File

@@ -2,7 +2,7 @@
* @Author: yehuozhili
* @Date: 2021-08-09 15:15:25
* @LastEditors: yehuozhili
* @LastEditTime: 2022-01-12 17:44:22
* @LastEditTime: 2022-04-01 13:41:34
* @FilePath: \dooringx\packages\dooringx-lib\src\components\timeLine\timeline.tsx
*/
import deepcopy from 'deepcopy';
@@ -457,7 +457,13 @@ export function TimeLine(props: TimeLineProps) {
}}
onMouseDown={(e) => {
const dom = e.target as HTMLDivElement;
if (!(dom.className && dom.className.indexOf('yh-timeline-item-mainblock') > -1)) {
if (
!(
dom.className &&
dom.className.indexOf &&
dom.className.indexOf('yh-timeline-item-mainblock') > -1
)
) {
resetCurrentMoveItemId();
}
}}

View File

@@ -6,7 +6,7 @@
* @FilePath: \dooringx\packages\dooringx-lib\src\config\index.tsx
*/
import React from 'react';
import { IBlockType, IStoreData } from '../core/store/storetype';
import { IBlockType, IMainStoreData, IStoreData } from '../core/store/storetype';
import { ComponentClass, FunctionComponent, ReactNode } from 'react';
import { ComponentItemFactory } from '../core/components/abstract';
import { marklineConfig } from '../core/markline/marklineConfig';
@@ -34,6 +34,7 @@ import { VerticalAlignMiddleOutlined } from '@ant-design/icons';
import { wrapperMoveState } from '../components/wrapperMove/event';
import { wrapperMoveState as iframeWrapperMoveState } from '../components/IframeWrapperMove/event';
import { TimeLineConfigType, TimeLineNeedleConfigType } from '../components/timeLine/timeline';
import { AnimateFactory } from '../core/AnimateFactory';
// 组件部分
/**
@@ -158,7 +159,7 @@ export interface InitConfig {
containerIcon: ReactNode;
}
export const defaultStore: IStoreData = {
export const defaultStore: IMainStoreData = {
container: {
width: 375,
height: 667,
@@ -173,6 +174,7 @@ export const defaultStore: IStoreData = {
title: 'dooring',
bodyColor: 'rgba(255,255,255,1)',
script: [],
customAnimate: [],
},
modalConfig: {},
};
@@ -337,6 +339,7 @@ export class UserConfig {
public componentRegister = new ComponentRegister();
public formRegister = new FormComponentRegister();
public storeChanger = new StoreChanger();
public animateFactory = new AnimateFactory();
public componentCache = {};
public asyncComponentUrlMap = {} as AsyncCacheComponentType;
public marklineConfig = marklineConfig;

View File

@@ -0,0 +1,198 @@
import UserConfig from '../../config';
import { CustomAnimateObj, IMainStoreData, IStoreData } from '../store/storetype';
import { deepCopy } from '../utils';
/**
*
* opacity: 100
percent: 0
positionX: 0
positionY: 0
rotate: 0
scale: 100
* @export 转换使用
* @interface TransformItemObj
*/
export interface TransformItemObj {
opacity: number;
percent: number;
positionX: number;
positionY: number;
rotate: number;
scale: number;
}
/**
*
*
* @export 用户输入对象
* @interface TransformItem
*/
export interface TransformItem {
displayName: string;
animateName: string;
keyframes: TransformItemObj[];
}
export class AnimateFactory {
constructor(public customAnimateName: Array<CustomAnimateObj> = []) {}
getCustomAnimateName() {
return this.customAnimateName;
}
getStyleSheets() {
return document.styleSheets;
}
/**
*
* 插入动画
* @param {string} ruleText
* @memberof AnimateFactory
*/
inserKeyframeAnimate(ruleText: string) {
const sheets = this.getStyleSheets();
if (sheets.length === 0) {
let style = document.createElement('style');
style.appendChild(document.createTextNode(''));
document.head.appendChild(style);
}
// const len = sheets.length;
// let ss: number | null = null;
// let st: number | null = null;
// for (let i = 0; i < len; i++) {
// for (let k = 0; k < sheets[i].cssRules.length; k++) {
// const rule = sheets[i].cssRules[k] as CSSKeyframesRule;
// const name = rule?.name;
// if (name && name === keyframeName) {
// // 删除该keyframe
// ss = i;
// st = k;
// }
// }
// }
// if (ss !== null && st !== null) {
// sheets[ss].deleteRule(st);
// }
// let sheet = sheets[ss ? ss : sheets.length - 1] as CSSStyleSheet;
let sheet = sheets[0] as CSSStyleSheet; // 末尾的经常存在重复覆盖的问题
sheet.insertRule(ruleText, sheet.cssRules.length);
}
/**
*
* 删除keyframe
* @param {string} animateName
* @returns
* @memberof AnimateFactory
*/
deleteKeyFrameAnimate(animateName: string) {
const sheets = this.getStyleSheets();
if (sheets.length === 0) {
return;
}
const sheet = sheets[0] as CSSStyleSheet;
const len = sheet.cssRules.length;
let ss = null;
for (let i = 0; i < len; i++) {
const rule = sheet.cssRules[i] as CSSKeyframesRule;
const name = rule?.name;
if (name && name === animateName) {
ss = i;
}
}
if (ss !== null) {
sheet.deleteRule(ss);
}
}
/**
*
* 配置时使用
* @param {Array<CustomAnimateObj>} [customAnimateNameArr=[]]
* @memberof AnimateFactory
*/
addCustomAnimate(customAnimateNameArr: Array<CustomAnimateObj> = []) {
this.customAnimateName = [...this.customAnimateName, ...customAnimateNameArr];
}
/**
*
* 删除使用animateName 防止displayName重名 用完需要同步store
* @param {string} animateName
* @memberof AnimateFactory
*/
deleteCustomAnimate(animateName: string) {
this.customAnimateName = this.customAnimateName.filter((v) => v.animateName !== animateName);
}
/**
*
* 从配置项插入动画 导入设置
* @memberof AnimateFactory
*/
fromArrInsertKeyFrame(customAnimateName: Array<CustomAnimateObj> = this.customAnimateName) {
customAnimateName.forEach((v) => {
this.inserKeyframeAnimate(v.keyframe);
});
}
/**
*
* 将this.customAnimateName写入store
* @memberof AnimateFactory
*/
syncToStore(config: UserConfig) {
// 先判断global的位置
const store = config.getStore();
let data: IStoreData;
const isEdit = config.getStoreChanger().isEdit();
if (isEdit) {
const origin = config.getStoreChanger().getOrigin()!;
data = origin.data[origin.current];
} else {
data = store.getData();
}
const copy: IMainStoreData = deepCopy(data);
const originGlobal = copy.globalState as IMainStoreData['globalState'];
originGlobal.customAnimate = [...this.customAnimateName];
if (isEdit) {
config.getStoreChanger().updateOrigin(copy);
} else {
store.setData(copy);
}
}
/**
*
* 将用户输入转换为新的动画
* @param {TransformItem} item
* @memberof AnimateFactory
*/
addUserInputIntoCustom(item: TransformItem, config: UserConfig) {
// 先转换keyframe
const keyframeItem = item.keyframes.map((v) => {
return `${v.percent}% {
transform:translate(${v.positionX}px, ${v.positionY}px) scale(${(v.scale / 100).toFixed(
2
)}) rotate(${v.rotate}deg);
}`;
});
const keyframe = `@keyframes ${item.animateName} {
${keyframeItem.join(' ')}
}`;
const customAnimateNameArr: CustomAnimateObj[] = [
{
displayName: item.displayName,
keyframe,
animateName: item.animateName,
},
];
// 添加内置
this.addCustomAnimate(customAnimateNameArr);
// 插入动画
this.inserKeyframeAnimate(keyframe);
// 写入store
this.syncToStore(config);
}
}

View File

@@ -8,6 +8,15 @@
import { EventCenterMapType } from '../eventCenter';
export interface GlobalState {
[key: string]: any;
customAnimate: CustomAnimateObj[];
containerColor: string;
title: string;
bodyColor: string;
script: string[];
}
export interface IStoreData {
container: {
width: number;
@@ -19,6 +28,10 @@ export interface IStoreData {
globalState: Record<string, any>;
modalConfig: Record<string, any>;
}
export interface IMainStoreData extends IStoreData {
globalState: GlobalState;
}
export interface AnimateItem {
uid: string;
animationName: string;
@@ -28,6 +41,12 @@ export interface AnimateItem {
animationTimingFunction: string;
}
export interface CustomAnimateObj {
displayName: string;
animateName: string;
keyframe: string;
}
export interface IBlockType {
id: string;
name: string;

View File

@@ -82,6 +82,12 @@ export class StoreChanger {
return this.map[ORIGIN];
}
/**
* 判断是否在编辑模式。
* 一次也没进行编辑时storeChanger中未存store所以只能判断去获取。
* @return {*}
* @memberof StoreChanger
*/
isEdit() {
if (storeChangerState.modalEditName !== '') {
return true;