@versa_ai/vmml-editor 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +335 -0
- package/CHANGELOG.md +16 -0
- package/README.md +1 -0
- package/biome.json +7 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2675 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2673 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +48 -0
- package/postcss.config.js +3 -0
- package/src/assets/css/closeLayer.scss +50 -0
- package/src/assets/css/colorSelector.scss +59 -0
- package/src/assets/css/editorTextMenu.less +130 -0
- package/src/assets/css/editorTextMenu.scss +149 -0
- package/src/assets/css/index.scss +252 -0
- package/src/assets/css/loading.scss +31 -0
- package/src/assets/css/maxTextLayer.scss +31 -0
- package/src/assets/img/icon_Brush.png +0 -0
- package/src/assets/img/icon_Change.png +0 -0
- package/src/assets/img/icon_Cut.png +0 -0
- package/src/assets/img/icon_Face.png +0 -0
- package/src/assets/img/icon_Graffiti.png +0 -0
- package/src/assets/img/icon_Mute.png +0 -0
- package/src/assets/img/icon_Refresh.png +0 -0
- package/src/assets/img/icon_Text1.png +0 -0
- package/src/assets/img/icon_Text2.png +0 -0
- package/src/assets/img/icon_Volume.png +0 -0
- package/src/assets/img/icon_Word.png +0 -0
- package/src/components/CloseLayer.tsx +25 -0
- package/src/components/ColorSelector.tsx +90 -0
- package/src/components/Controls.tsx +32 -0
- package/src/components/EditorCanvas.tsx +566 -0
- package/src/components/Loading.tsx +16 -0
- package/src/components/MaxTextLayer.tsx +27 -0
- package/src/components/SeekBar.tsx +126 -0
- package/src/components/TextMenu.tsx +332 -0
- package/src/components/VideoMenu.tsx +49 -0
- package/src/index.tsx +551 -0
- package/src/utils/HistoryClass.ts +131 -0
- package/src/utils/VmmlConverter.ts +339 -0
- package/src/utils/const.ts +10 -0
- package/src/utils/keyBoardUtils.ts +199 -0
- package/src/utils/usePeekControl.ts +242 -0
- package/tsconfig.json +5 -0
- package/tsup.config.ts +14 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import { interpolate } from "remotion"
|
|
3
|
+
import { signIcon } from "../utils/const"
|
|
4
|
+
|
|
5
|
+
const getFrameFromX = (
|
|
6
|
+
clientX: number,
|
|
7
|
+
durationInFrames: number,
|
|
8
|
+
width: number,
|
|
9
|
+
) => {
|
|
10
|
+
const pos = clientX;
|
|
11
|
+
const frame = Math.round(
|
|
12
|
+
interpolate(pos, [0, width], [0, durationInFrames - 1], {
|
|
13
|
+
extrapolateLeft: 'clamp',
|
|
14
|
+
extrapolateRight: 'clamp',
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
return frame;
|
|
18
|
+
};
|
|
19
|
+
console.log(2222)
|
|
20
|
+
const SeekBar = ({ player, vmmlRef, frame, durationInFrames, intoEdit, setDragState, signList }: any ) => {
|
|
21
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const [dragging, setDragging] = useState<any>({
|
|
23
|
+
dragging: false,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const fillStyle = useMemo(() => {
|
|
27
|
+
return {
|
|
28
|
+
width: `${(frame / (durationInFrames - 1)) * 100}%`,
|
|
29
|
+
}
|
|
30
|
+
}, [frame, durationInFrames]);
|
|
31
|
+
|
|
32
|
+
const cirleStyle = useMemo(() => {
|
|
33
|
+
return {
|
|
34
|
+
left: `calc(${(frame / (durationInFrames - 1)) * 100}% - 1.6vw)`,
|
|
35
|
+
}
|
|
36
|
+
}, [frame, durationInFrames])
|
|
37
|
+
|
|
38
|
+
const onPointerDown = useCallback((e: React.PointerEvent) => {
|
|
39
|
+
if (e.button !== 0) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const { left, width } = containerRef.current?.getBoundingClientRect() as any;
|
|
43
|
+
const _frame = getFrameFromX(
|
|
44
|
+
e.clientX - left,
|
|
45
|
+
durationInFrames,
|
|
46
|
+
width,
|
|
47
|
+
);
|
|
48
|
+
setDragState(1);
|
|
49
|
+
setDragging({
|
|
50
|
+
dragging: true,
|
|
51
|
+
wasPlaying: player.isPlaying(),
|
|
52
|
+
});
|
|
53
|
+
player.pause();
|
|
54
|
+
player.seekTo(_frame);
|
|
55
|
+
vmmlRef.current && vmmlRef.current.onSeekStart();
|
|
56
|
+
}, [player, durationInFrames]);
|
|
57
|
+
|
|
58
|
+
const onPointerMove = useCallback((e: PointerEvent) => {
|
|
59
|
+
if (!dragging.dragging) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const { left, width } = containerRef.current?.getBoundingClientRect() as any;
|
|
63
|
+
const _frame = getFrameFromX(
|
|
64
|
+
e.clientX - left,
|
|
65
|
+
durationInFrames,
|
|
66
|
+
width,
|
|
67
|
+
);
|
|
68
|
+
setDragState(2);
|
|
69
|
+
player.seekTo(_frame);
|
|
70
|
+
}, [player, dragging.dragging, durationInFrames]);
|
|
71
|
+
|
|
72
|
+
const onPointerUp = (e: any) => {
|
|
73
|
+
setDragging({
|
|
74
|
+
dragging: false,
|
|
75
|
+
});
|
|
76
|
+
if (!dragging.dragging) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
setDragState(3);
|
|
80
|
+
intoEdit();
|
|
81
|
+
vmmlRef.current && vmmlRef.current.onSeekEnd();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const onClickSign = (sign: any) => {
|
|
85
|
+
player.pause();
|
|
86
|
+
player.seekTo(sign.inFrame);
|
|
87
|
+
vmmlRef.current && vmmlRef.current.onSeekStart();
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
setDragState(4);
|
|
90
|
+
intoEdit(sign.inFrame);
|
|
91
|
+
vmmlRef.current && vmmlRef.current.onSeekEnd();
|
|
92
|
+
}, 100)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!dragging.dragging) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const body = document.querySelector("body");
|
|
100
|
+
if (body) {
|
|
101
|
+
body.addEventListener('pointermove', onPointerMove);
|
|
102
|
+
body.addEventListener('pointerup', onPointerUp);
|
|
103
|
+
return () => {
|
|
104
|
+
body.removeEventListener('pointermove', onPointerMove);
|
|
105
|
+
body.removeEventListener('pointerup', onPointerUp);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}, [dragging.dragging, onPointerMove, onPointerUp]);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div ref={containerRef} className="seekbar-container">
|
|
112
|
+
<div className="seekbar-background">
|
|
113
|
+
<div className="seekbar-fill" onPointerDown={onPointerDown}></div>
|
|
114
|
+
<div className="seekbar-line" style={fillStyle}></div>
|
|
115
|
+
<div className="seekbar-signs">
|
|
116
|
+
{signList.map((item: any, index: number) => (
|
|
117
|
+
<img key={index} onClick={() => onClickSign(item)} style={{ left: `calc(${item.inFrame / durationInFrames * 100}% - 6px)` }} className="seekbar-sign" src={signIcon} alt="" />
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
<div className="seekbar-cirle" style={cirleStyle} onPointerDown={onPointerDown} />
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default SeekBar
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
2
|
+
import "../assets/css/editorTextMenu.scss";
|
|
3
|
+
import { closeIcon } from "../utils/const";
|
|
4
|
+
import { isIOS } from '@versa_ai/vmml-utils'
|
|
5
|
+
import { onKeyBoardAction, watchKeyBoard, canAdapter } from "../utils/keyBoardUtils";
|
|
6
|
+
import ColorSelector from "./ColorSelector";
|
|
7
|
+
// 处理文本内容,总共40个字
|
|
8
|
+
const formatText = (text: string, maxLengthBase = 40): string => {
|
|
9
|
+
// 正则表达式匹配所有类型的换行符
|
|
10
|
+
const newlineRegex = /(\r\n|\r|\n)/g;
|
|
11
|
+
const matches = text.match(newlineRegex)?.length || 0;
|
|
12
|
+
const maxLength = maxLengthBase + matches
|
|
13
|
+
let trimmedText = text.substring(0, maxLength)
|
|
14
|
+
if ((text.length - matches) === maxLengthBase) {
|
|
15
|
+
// 正则表达式去除尾部的换行符
|
|
16
|
+
trimmedText = trimmedText.replace(/(\r\n|\r|\n)+$/g, '');
|
|
17
|
+
}
|
|
18
|
+
return trimmedText;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const TextMenu = forwardRef(({ createText, textClose, textInfo, showTextButtons }: any, ref: any) => {
|
|
22
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
23
|
+
const headerRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
25
|
+
const coverRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
const mappingarea = useRef<HTMLDivElement>(null);
|
|
27
|
+
const [textContent, setTextContent] = useState("");
|
|
28
|
+
const [bgColor, setBgColor] = useState("transparent");
|
|
29
|
+
const [textColor, setTextColor] = useState("rgb(248,82,81)");
|
|
30
|
+
const [textPos, setTextPos] = useState<any>({ left: 0, top: 0, angle: 0, scaleX: 0.75, scaleY: 0.75, zoomX: 0, zoomY: 0 });
|
|
31
|
+
const [textBasicInfo, setTextBasicInfo] = useState<any>({
|
|
32
|
+
colorName: "red",
|
|
33
|
+
colorValue: "rgb(248,82,81)",
|
|
34
|
+
isBack: false,
|
|
35
|
+
textAlign: 'left'
|
|
36
|
+
});
|
|
37
|
+
const colorSelectorRef = useRef<any>(null);
|
|
38
|
+
const [textContentOnShow, setTextContentOnShow] = useState<any>([]);
|
|
39
|
+
const [scrollYValue, setScrollYValue] = useState(() => {
|
|
40
|
+
return Number.parseInt(localStorage.getItem('scrollYValue') as string) || null;
|
|
41
|
+
});
|
|
42
|
+
const scrollYValueRef = useRef(scrollYValue);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const callback = (status: any) => {
|
|
45
|
+
if (scrollYValueRef.current === null) {
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
const newScrollYValue = window.scrollY;
|
|
48
|
+
setScrollYValue(newScrollYValue);
|
|
49
|
+
scrollYValueRef.current = newScrollYValue;
|
|
50
|
+
localStorage.setItem('scrollYValue', newScrollYValue.toString());
|
|
51
|
+
}, 150);
|
|
52
|
+
}
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
onKeyBoardAction(
|
|
55
|
+
status,
|
|
56
|
+
headerRef.current,
|
|
57
|
+
textareaRef.current,
|
|
58
|
+
coverRef.current,
|
|
59
|
+
mappingarea.current
|
|
60
|
+
);
|
|
61
|
+
}, 200);
|
|
62
|
+
};
|
|
63
|
+
const removeListeners = watchKeyBoard(callback);
|
|
64
|
+
// 清理函数,用于移除事件监听器
|
|
65
|
+
return () => {
|
|
66
|
+
removeListeners();
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
//设置同步滚动
|
|
71
|
+
const syncScroll = () => {
|
|
72
|
+
if (textareaRef.current && mappingarea.current) {
|
|
73
|
+
const scrollPosition = textareaRef.current.scrollTop;
|
|
74
|
+
mappingarea.current.scrollTop = scrollPosition;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
if (textareaRef.current) {
|
|
78
|
+
// 添加滚动监听器到text-input
|
|
79
|
+
textareaRef.current.addEventListener("scroll", syncScroll);
|
|
80
|
+
// 添加输入监听器到textarea
|
|
81
|
+
textareaRef.current.addEventListener("input", syncScroll);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 清理函数,移除监听器
|
|
85
|
+
return () => {
|
|
86
|
+
if (textareaRef.current) {
|
|
87
|
+
textareaRef.current.removeEventListener("scroll", syncScroll);
|
|
88
|
+
textareaRef.current.removeEventListener("input", syncScroll);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}, []);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
let lines = [];
|
|
94
|
+
//判断字符里是否存在\r
|
|
95
|
+
if (textContent.includes("\r")) {
|
|
96
|
+
lines = textContent.split("\r");
|
|
97
|
+
} else {
|
|
98
|
+
lines = textContent.split("\n");
|
|
99
|
+
}
|
|
100
|
+
setTextContentOnShow(lines);
|
|
101
|
+
}, [textContent]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (Object.keys(textInfo).length > 0) {
|
|
105
|
+
setText(textInfo);
|
|
106
|
+
} else {
|
|
107
|
+
resetInfo();
|
|
108
|
+
}
|
|
109
|
+
}, [textInfo]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!isIOS()) {
|
|
113
|
+
if (mappingarea.current && textareaRef.current) {
|
|
114
|
+
textareaRef.current.style.position = 'static'
|
|
115
|
+
mappingarea.current.style.maxHeight = textareaRef.current.style.height = '50vh'
|
|
116
|
+
mappingarea.current.style.maxWidth = getComputedStyle(textareaRef.current).width;
|
|
117
|
+
textareaRef.current.focus();
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
if (textareaRef.current) {
|
|
121
|
+
textareaRef.current.focus();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, [])
|
|
125
|
+
const handleOnChange = (e: any) => {
|
|
126
|
+
const inputValue = e.target.value;
|
|
127
|
+
setTextContent(formatText(inputValue));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleSubmit = (e: any) => {
|
|
131
|
+
if (showTextButtons) {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
}
|
|
134
|
+
if (formRef.current) {
|
|
135
|
+
const formData = new FormData(formRef.current);
|
|
136
|
+
// 也可以使用普通的对象:
|
|
137
|
+
const formJson = Object.fromEntries((formData as any).entries());
|
|
138
|
+
const { input_content } = formJson;
|
|
139
|
+
renderText(input_content);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const handleChangeColor = (item: any) => {
|
|
143
|
+
setColor(item);
|
|
144
|
+
};
|
|
145
|
+
const onBackGroundChange = (currentColorItem: string) => {
|
|
146
|
+
setColor(currentColorItem);
|
|
147
|
+
};
|
|
148
|
+
const setColor = (colorItem: any) => {
|
|
149
|
+
if (textareaRef.current) {
|
|
150
|
+
textareaRef.current.focus();
|
|
151
|
+
}
|
|
152
|
+
const { current } = colorSelectorRef;
|
|
153
|
+
const { colorName, colorValue } = colorItem;
|
|
154
|
+
const isBack = current ? current.getBackState() : false;
|
|
155
|
+
const textBasic = {
|
|
156
|
+
colorName,
|
|
157
|
+
colorValue,
|
|
158
|
+
isBack,
|
|
159
|
+
textAlign: textBasicInfo.textAlign
|
|
160
|
+
};
|
|
161
|
+
setTextBasicInfo(textBasic);
|
|
162
|
+
//给文字设置颜色
|
|
163
|
+
if (!isBack) {
|
|
164
|
+
setBgColor("transparent");
|
|
165
|
+
setTextColor(colorValue);
|
|
166
|
+
} else {
|
|
167
|
+
//给背景设置颜色
|
|
168
|
+
//点击切换颜色「非白色背景」,字都统一白色,背景变化对应颜色,若选择「白色背景」字变成黑色
|
|
169
|
+
setBgColor(colorValue);
|
|
170
|
+
colorName === "white" || colorValue === 'rgba(255, 255, 255, 1)' ? setTextColor("#000") : setTextColor("#fff");
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const setText = (textInfo: any) => {
|
|
174
|
+
const { bgColor, text, textColor, left, top, angle, scaleX, scaleY, zoomX, zoomY, textBasicInfo, width, height } = textInfo;
|
|
175
|
+
const { isBack, colorName, colorValue } = textBasicInfo;
|
|
176
|
+
const fillColor = text === '请输入文案' && textColor === 'rgba(0, 0, 0, 0)' ? 'rgba(255, 255, 255, 1)' : textColor;
|
|
177
|
+
setBgColor(bgColor);
|
|
178
|
+
setTextColor(fillColor);
|
|
179
|
+
setTextContent(text);
|
|
180
|
+
setTextBasicInfo(textBasicInfo);
|
|
181
|
+
setTextPos({ left, top, angle, scaleX, scaleY, zoomX, zoomY, width, height });
|
|
182
|
+
const { current } = colorSelectorRef;
|
|
183
|
+
if (!current) return;
|
|
184
|
+
current.setIsSetBack(isBack);
|
|
185
|
+
current.setCurrentColorItem({ colorName, colorValue });
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const resetInfo = () => {
|
|
189
|
+
//文字编辑缺省设置
|
|
190
|
+
setBgColor("transparent");
|
|
191
|
+
setTextColor("rgb(248,82,81)");
|
|
192
|
+
setTextContent("");
|
|
193
|
+
setTextPos({ left: 0, top: 0, angle: 0, scaleX: 0.75, scaleY: 0.75, zoomX: 0, zoomY: 0 });
|
|
194
|
+
};
|
|
195
|
+
const close = () => {
|
|
196
|
+
textClose(textInfo.id);
|
|
197
|
+
};
|
|
198
|
+
const renderText = (input_content?: string) => {
|
|
199
|
+
const { current } = mappingarea;
|
|
200
|
+
if (current) {
|
|
201
|
+
//获取映射区域的宽高传入画布,作为fabric文字的宽高
|
|
202
|
+
//获取计算属性
|
|
203
|
+
//在close时需要使用传入的textpos
|
|
204
|
+
const computedStyle = window.getComputedStyle(current);
|
|
205
|
+
const { width, height } = computedStyle;
|
|
206
|
+
setTextPos(
|
|
207
|
+
Object.assign(textPos, { width: Number(width.split("px")[0]), height: Number(height.split("px")[0]) }),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
//判断是否空字符串,并且不全是空格
|
|
211
|
+
if (input_content && input_content.trim() !== "") {
|
|
212
|
+
const { id, fontFamily, fontAssetUrl } = textInfo;
|
|
213
|
+
//传递给父组件文字属性
|
|
214
|
+
//input_content带有换行符,所以不能用textContent传到player
|
|
215
|
+
createText({ textContent: input_content, bgColor, textColor, position: textPos, textBasicInfo, id, fontFamily, fontAssetUrl });
|
|
216
|
+
} else {
|
|
217
|
+
textClose(textInfo.id);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
useImperativeHandle(ref, () => ({
|
|
221
|
+
setText,
|
|
222
|
+
close,
|
|
223
|
+
handleSubmit
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<form method="post" onSubmit={handleSubmit} ref={formRef}>
|
|
228
|
+
<div className="overlay">
|
|
229
|
+
{/* 顶部 */}
|
|
230
|
+
{
|
|
231
|
+
showTextButtons && (
|
|
232
|
+
<div className="text-header" ref={headerRef}>
|
|
233
|
+
<div onClick={() => close()} className="close">
|
|
234
|
+
<img crossOrigin="anonymous" src={closeIcon} alt="close" />
|
|
235
|
+
</div>
|
|
236
|
+
<button type="submit" className="next-button">
|
|
237
|
+
完成
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
{
|
|
243
|
+
!isIOS() && (
|
|
244
|
+
<div className="edit_container" style={{
|
|
245
|
+
width: '100%', position: 'relative', paddingLeft: '4.4vw',
|
|
246
|
+
paddingRight: '3.2vw', display: 'flex'
|
|
247
|
+
}}>
|
|
248
|
+
{/* 编辑区 */}
|
|
249
|
+
<textarea
|
|
250
|
+
ref={textareaRef}
|
|
251
|
+
cols={20}
|
|
252
|
+
wrap="hard"
|
|
253
|
+
name="input_content"
|
|
254
|
+
className="text-input"
|
|
255
|
+
value={textContent}
|
|
256
|
+
onChange={handleOnChange}
|
|
257
|
+
style={{ color: textColor, width: '94%', fontFamily: textInfo.fontFamily }}
|
|
258
|
+
/>
|
|
259
|
+
{/* 映射颜色区域 */}
|
|
260
|
+
<div
|
|
261
|
+
className="mappingarea"
|
|
262
|
+
ref={mappingarea}
|
|
263
|
+
contentEditable
|
|
264
|
+
suppressContentEditableWarning
|
|
265
|
+
style={{
|
|
266
|
+
background: bgColor, position: 'absolute',
|
|
267
|
+
top: '0',
|
|
268
|
+
zIndex: '-1',
|
|
269
|
+
left: '4.4vw',
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
{/* 颜色适应每行方案 */}
|
|
273
|
+
{textContentOnShow.map((text: string, index: number) => {
|
|
274
|
+
return (
|
|
275
|
+
<span className="mappingarea_span" key={index} style={{ maxWidth: '100%' }}>
|
|
276
|
+
{text ? text : " "}
|
|
277
|
+
</span>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
{
|
|
285
|
+
isIOS() && (
|
|
286
|
+
<>
|
|
287
|
+
{/* 编辑区 */}
|
|
288
|
+
<textarea
|
|
289
|
+
ref={textareaRef}
|
|
290
|
+
cols={20}
|
|
291
|
+
wrap="hard"
|
|
292
|
+
name="input_content"
|
|
293
|
+
className="text-input"
|
|
294
|
+
value={textContent}
|
|
295
|
+
onChange={handleOnChange}
|
|
296
|
+
style={{ color: textColor, fontFamily: textInfo.fontFamily }}
|
|
297
|
+
/>
|
|
298
|
+
{/* 映射颜色区域 */}
|
|
299
|
+
<div
|
|
300
|
+
className="mappingarea"
|
|
301
|
+
ref={mappingarea}
|
|
302
|
+
contentEditable
|
|
303
|
+
suppressContentEditableWarning
|
|
304
|
+
style={{ background: bgColor }}
|
|
305
|
+
>
|
|
306
|
+
{/* 颜色适应每行方案 */}
|
|
307
|
+
{textContentOnShow.map((text: string, index: number) => {
|
|
308
|
+
return (
|
|
309
|
+
<span className="mappingarea_span" key={index}>
|
|
310
|
+
{text ? text : " "}
|
|
311
|
+
</span>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
314
|
+
</div>
|
|
315
|
+
</>
|
|
316
|
+
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
{/* 颜色选择器 */}
|
|
320
|
+
<div ref={coverRef} className="cover">
|
|
321
|
+
<ColorSelector
|
|
322
|
+
ref={colorSelectorRef}
|
|
323
|
+
onColorChange={handleChangeColor}
|
|
324
|
+
onBackGroundChange={onBackGroundChange}
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</form>
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
export default TextMenu;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
const VideoMenu = ({ createImage, videoMenus, menuState }: any) => {
|
|
4
|
+
|
|
5
|
+
const [active, setActive] = useState<number>();
|
|
6
|
+
const [menus, setMenus] = useState<any>([])
|
|
7
|
+
const [items, setItems] = useState<any>([]);
|
|
8
|
+
|
|
9
|
+
const onClickClass = ({ id, emojiList }: any) => {
|
|
10
|
+
setActive(id);
|
|
11
|
+
setItems(emojiList)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const onClickItem = ({ emojiId, fileUrl }: any) => {
|
|
15
|
+
if (menuState === "video") {
|
|
16
|
+
createImage(fileUrl, emojiId);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setMenus(videoMenus);
|
|
22
|
+
if (videoMenus.length) {
|
|
23
|
+
onClickClass(videoMenus[0]);
|
|
24
|
+
}
|
|
25
|
+
}, [videoMenus]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={`editor-video-menu ${menuState === "video" ? "active" : ""}`}>
|
|
29
|
+
<div className="editor-video-menu-header">
|
|
30
|
+
{
|
|
31
|
+
menus.map((item: any) => (
|
|
32
|
+
<div className={ active === item.id ? "active": "" } key={item.id} onClick={() => onClickClass(item)} >{item.classifyType}</div>
|
|
33
|
+
))
|
|
34
|
+
}
|
|
35
|
+
</div>
|
|
36
|
+
<div className="editor-video-menu-itmes">
|
|
37
|
+
{
|
|
38
|
+
items.map((item: any) => (
|
|
39
|
+
<div key={item.emojiId} onClick={() => onClickItem(item)}>
|
|
40
|
+
<img loading="lazy" src={item.fileUrl.thumbnailUrl} alt="素材" />
|
|
41
|
+
</div>
|
|
42
|
+
))
|
|
43
|
+
}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default VideoMenu
|