@chenyomi/leafer-htmltext-editor 1.0.0
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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/TextEditTool/index.d.ts +17 -0
- package/dist/TextEditTool/index.d.ts.map +1 -0
- package/dist/TextEditTool/index.js +138 -0
- package/dist/TextEditTool/utils.d.ts +8 -0
- package/dist/TextEditTool/utils.d.ts.map +1 -0
- package/dist/TextEditTool/utils.js +173 -0
- package/dist/TextEditor.d.ts +27 -0
- package/dist/TextEditor.d.ts.map +1 -0
- package/dist/TextEditor.js +166 -0
- package/dist/esm/TextEditTool/index.d.ts +17 -0
- package/dist/esm/TextEditTool/index.d.ts.map +1 -0
- package/dist/esm/TextEditTool/index.js +135 -0
- package/dist/esm/TextEditTool/utils.d.ts +8 -0
- package/dist/esm/TextEditTool/utils.d.ts.map +1 -0
- package/dist/esm/TextEditTool/utils.js +165 -0
- package/dist/esm/TextEditor.d.ts +27 -0
- package/dist/esm/TextEditor.d.ts.map +1 -0
- package/dist/esm/TextEditor.js +163 -0
- package/dist/esm/fonts/font.d.ts +17 -0
- package/dist/esm/fonts/font.d.ts.map +1 -0
- package/dist/esm/fonts/font.js +68 -0
- package/dist/esm/fonts/utils.d.ts +9 -0
- package/dist/esm/fonts/utils.d.ts.map +1 -0
- package/dist/esm/fonts/utils.js +170 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/utils.d.ts +3 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +284 -0
- package/dist/fonts/font.d.ts +17 -0
- package/dist/fonts/font.d.ts.map +1 -0
- package/dist/fonts/font.js +72 -0
- package/dist/fonts/utils.d.ts +9 -0
- package/dist/fonts/utils.d.ts.map +1 -0
- package/dist/fonts/utils.js +180 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +79 -0
- package/dist/index.js +92 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +289 -0
- package/package.json +55 -0
- package/src/TextEditTool/index.ts +145 -0
- package/src/TextEditTool/utils.ts +216 -0
- package/src/TextEditor.ts +200 -0
- package/src/fonts/font.ts +86 -0
- package/src/fonts/utils.ts +232 -0
- package/src/index.ts +92 -0
- package/src/utils.ts +331 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const FONT_CSS_TAG = "data-fonts"
|
|
2
|
+
const FONT_CSS_TAG_BASE64 = "base64-fonts"
|
|
3
|
+
|
|
4
|
+
export async function fetchFontAsBase64(url: string): Promise<string> {
|
|
5
|
+
const response = await fetch(url);
|
|
6
|
+
if (!response.ok) {
|
|
7
|
+
throw new Error(`Failed to fetch font from ${url}`);
|
|
8
|
+
}
|
|
9
|
+
// 获取文件类型
|
|
10
|
+
const contentType = response.headers.get('Content-Type');
|
|
11
|
+
let mimeType: string;
|
|
12
|
+
|
|
13
|
+
// 根据 Content-Type 设置 MIME 类型
|
|
14
|
+
if (contentType?.includes('font/woff2')) {
|
|
15
|
+
mimeType = 'font/woff2';
|
|
16
|
+
} else if (contentType?.includes('font/woff')) {
|
|
17
|
+
mimeType = 'font/woff';
|
|
18
|
+
} else if (contentType?.includes('font/ttf') || contentType?.includes('font/sfnt')) {
|
|
19
|
+
mimeType = 'font/ttf';
|
|
20
|
+
} else if (contentType?.includes('application/octet-stream')) {
|
|
21
|
+
// 如果 Content-Type 是通用的二进制流,尝试从文件扩展名推断类型
|
|
22
|
+
const extension = url.split('.').pop()?.toLowerCase();
|
|
23
|
+
if (extension === 'woff2') {
|
|
24
|
+
mimeType = 'font/woff2';
|
|
25
|
+
} else if (extension === 'woff') {
|
|
26
|
+
mimeType = 'font/woff';
|
|
27
|
+
} else if (extension === 'ttf') {
|
|
28
|
+
mimeType = 'font/ttf';
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error(`Unsupported font file type: ${extension}`);
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error(`Unsupported Content-Type: ${contentType}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 将文件转换为 Base64
|
|
37
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
38
|
+
const base64String = arrayBufferToBase64(arrayBuffer);
|
|
39
|
+
|
|
40
|
+
// 返回 data: URL
|
|
41
|
+
return `data:${mimeType};charset=utf-8;base64,${base64String}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
45
|
+
let binary = '';
|
|
46
|
+
const bytes = new Uint8Array(buffer);
|
|
47
|
+
const len = bytes.byteLength;
|
|
48
|
+
for (let i = 0; i < len; i++) {
|
|
49
|
+
binary += String.fromCharCode(bytes[i]);
|
|
50
|
+
}
|
|
51
|
+
return window.btoa(binary);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 批量添加自定义字体样式
|
|
56
|
+
* @param fontList
|
|
57
|
+
*/
|
|
58
|
+
export function addCustomFonts(fontList: any = []) {
|
|
59
|
+
let styleTag = document.createElement('style');
|
|
60
|
+
styleTag.setAttribute(FONT_CSS_TAG, 'true');
|
|
61
|
+
let fontRules = fontList.map((font: any) => `@font-face {
|
|
62
|
+
font-family: "${font.name}";
|
|
63
|
+
src: local("${font.name}"), url("${font.download}")
|
|
64
|
+
}`).join('\n');
|
|
65
|
+
styleTag.textContent = fontRules;
|
|
66
|
+
document.head.appendChild(styleTag);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 获取自定义字体样式
|
|
71
|
+
*/
|
|
72
|
+
export function getCustomFontsStyle() {
|
|
73
|
+
const styleTag = document.querySelector('style[data-fonts]');
|
|
74
|
+
return styleTag?.textContent || '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 添加单个自定义字体样式
|
|
79
|
+
* @param font
|
|
80
|
+
*/
|
|
81
|
+
export function addCustomFont(font: any) {
|
|
82
|
+
let styleTag = document.querySelector('style[data-fonts]') as HTMLStyleElement | null;
|
|
83
|
+
// 如果不存在样式标签,则创建一个新的style标签
|
|
84
|
+
if (!styleTag) {
|
|
85
|
+
styleTag = document.createElement('style');
|
|
86
|
+
styleTag.setAttribute(FONT_CSS_TAG, 'true');
|
|
87
|
+
document.head.appendChild(styleTag);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!styleTag.sheet) return;
|
|
91
|
+
|
|
92
|
+
let existingFonts: string[] = [];
|
|
93
|
+
existingFonts = Array.from(styleTag.sheet.cssRules).map((rule: CSSRule) => {
|
|
94
|
+
const match = rule.cssText.match(/font-family: "([^"]+)"/);
|
|
95
|
+
return match ? match[1] : null;
|
|
96
|
+
}).filter((font): font is string => font !== null);
|
|
97
|
+
|
|
98
|
+
// 判断要添加的字体是否已经存在于样式表中
|
|
99
|
+
if (!existingFonts.includes(font.name)) {
|
|
100
|
+
// 创建新的 @font-face 规则
|
|
101
|
+
const newFontRule = `@font-face {
|
|
102
|
+
font-family: "${font.name}";
|
|
103
|
+
src: url("${font.download}");
|
|
104
|
+
}`;
|
|
105
|
+
|
|
106
|
+
// 插入新的 @font-face 规则到样式表中
|
|
107
|
+
try {
|
|
108
|
+
styleTag.sheet.insertRule(newFontRule, styleTag.sheet.cssRules.length);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error('Failed to insert font rule:', e);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 批量加载字体(要在生成完字体css后再调用次方法执行)
|
|
117
|
+
* @param fontNameList 字体名称
|
|
118
|
+
*/
|
|
119
|
+
export async function batchLoadFont(fontNameList: any = []) {
|
|
120
|
+
// FontFaceObserver 需要额外安装,这里注释掉
|
|
121
|
+
// 可以使用浏览器原生的 document.fonts API
|
|
122
|
+
try {
|
|
123
|
+
for (const fontFamily of fontNameList) {
|
|
124
|
+
await document.fonts.load(`16px ${fontFamily}`);
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.warn('Font loading failed:', e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getFontUrlFromCSS(selectors: string, fontFamilyName: string): string | null {
|
|
132
|
+
// 获取所有 <style> 标签
|
|
133
|
+
const styleTags = document.querySelectorAll(selectors);
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < styleTags.length; i++) {
|
|
136
|
+
const styleTag = styleTags[i];
|
|
137
|
+
// 获取 CSS 内容
|
|
138
|
+
const cssText = styleTag.textContent || '';
|
|
139
|
+
|
|
140
|
+
// 使用正则表达式匹配 @font-face 规则
|
|
141
|
+
const fontFaceRules = cssText.match(/@font-face\s*\{[^}]+\}/g);
|
|
142
|
+
|
|
143
|
+
if (fontFaceRules) {
|
|
144
|
+
for (const rule of fontFaceRules) {
|
|
145
|
+
// 提取 font-family
|
|
146
|
+
const fontFamilyMatch = rule.match(/font-family:\s*(["'])(.*?)\1/);
|
|
147
|
+
if (fontFamilyMatch && fontFamilyMatch[2] === fontFamilyName) {
|
|
148
|
+
// 提取 src
|
|
149
|
+
const srcMatch = rule.match(/src:\s*url\((["'])(.*?)\1\)/);
|
|
150
|
+
if (srcMatch) {
|
|
151
|
+
return srcMatch[2]; // 返回字体文件 URL
|
|
152
|
+
} else {
|
|
153
|
+
const srcMatch2 = rule.match(/src:\s*(?:local\([^)]+\)\s*,\s*)?url\((["'])(.*?)\1\)/);
|
|
154
|
+
if (srcMatch2) {
|
|
155
|
+
return srcMatch2[2];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null; // 未找到匹配的字体
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 添加单个自定义字体样式(Base64格式)
|
|
168
|
+
* @param font
|
|
169
|
+
*/
|
|
170
|
+
export async function addCustomFontBase64(font: any) {
|
|
171
|
+
const fontUrl = getFontUrlFromCSS(`style[${FONT_CSS_TAG}]`, font);
|
|
172
|
+
if (!fontUrl) {
|
|
173
|
+
console.warn('Font URL not found for:', font);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log('fontUrl=', fontUrl);
|
|
178
|
+
let styleTag = document.querySelector(`style[${FONT_CSS_TAG_BASE64}]`) as HTMLStyleElement | null;
|
|
179
|
+
// 如果不存在样式标签,则创建一个新的style标签
|
|
180
|
+
if (!styleTag) {
|
|
181
|
+
styleTag = document.createElement('style');
|
|
182
|
+
styleTag.setAttribute(FONT_CSS_TAG_BASE64, 'true');
|
|
183
|
+
document.head.appendChild(styleTag);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 确保样式标签已经被添加到文档中
|
|
187
|
+
if (!styleTag.sheet) {
|
|
188
|
+
await new Promise(resolve => setTimeout(resolve, 10)); // 等待样式标签被加载
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!styleTag.sheet) return;
|
|
192
|
+
|
|
193
|
+
let existingFonts = Array.from(styleTag.sheet.cssRules).map((rule: CSSRule) => {
|
|
194
|
+
const match = rule.cssText.match(/font-family: "([^"]+)"/);
|
|
195
|
+
return match ? match[1] : null;
|
|
196
|
+
}).filter((font): font is string => font !== null);
|
|
197
|
+
|
|
198
|
+
// 判断要添加的字体是否已经存在于样式表中
|
|
199
|
+
if (!existingFonts.includes(font)) {
|
|
200
|
+
const base64Url = await fetchFontAsBase64(fontUrl);
|
|
201
|
+
// 创建新的 @font-face 规则
|
|
202
|
+
const newFontRule = `@font-face {
|
|
203
|
+
font-family: "${font}";
|
|
204
|
+
src: local("${font}"), url("${base64Url}");
|
|
205
|
+
}`;
|
|
206
|
+
|
|
207
|
+
// 插入新的 @font-face 规则到样式表中
|
|
208
|
+
try {
|
|
209
|
+
styleTag.sheet.insertRule(newFontRule, styleTag.sheet.cssRules.length);
|
|
210
|
+
styleTag.textContent += newFontRule;
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.error('Failed to insert font rule:', e);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 获取自定义字体样式
|
|
219
|
+
*/
|
|
220
|
+
export function getBase64CustomFontsStyle() {
|
|
221
|
+
const styleTag = document.querySelector(`style[${FONT_CSS_TAG_BASE64}]`);
|
|
222
|
+
return styleTag?.textContent || '';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 加载单个字体并转换为 Base64 格式
|
|
227
|
+
* @param font 字体名称
|
|
228
|
+
* @returns Promise<void>
|
|
229
|
+
*/
|
|
230
|
+
export async function loadFont(font: string): Promise<void> {
|
|
231
|
+
await addCustomFontBase64(font);
|
|
232
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { TextEditor } from "./TextEditor";
|
|
2
|
+
export { TextEditor } from "./TextEditor";
|
|
3
|
+
import "./TextEditTool";
|
|
4
|
+
import { Plugin } from "@leafer-ui/core";
|
|
5
|
+
import { Delta } from "quill";
|
|
6
|
+
import Quill from "quill";
|
|
7
|
+
import "quill/dist/quill.core.css";
|
|
8
|
+
|
|
9
|
+
// 导出工具函数
|
|
10
|
+
export { updataHtmlText, setHTMLText } from "./utils";
|
|
11
|
+
export { fontManager, defaultFonts, FontManager } from "./fonts/font";
|
|
12
|
+
|
|
13
|
+
Plugin.add("leafer-htmltext-editor", "editor");
|
|
14
|
+
|
|
15
|
+
export function initTextEditorQuill(container?: HTMLElement) {
|
|
16
|
+
const textInner = document.getElementById("textInnerEditor");
|
|
17
|
+
if (!textInner) {
|
|
18
|
+
const el = document.createElement("div");
|
|
19
|
+
el.id = "textInnerEditor";
|
|
20
|
+
el.style.position = "fixed";
|
|
21
|
+
el.style.transformOrigin = "left top";
|
|
22
|
+
el.style.overflowWrap = "break-word";
|
|
23
|
+
el.style.wordBreak = "break-all";
|
|
24
|
+
el.style.visibility = "hidden";
|
|
25
|
+
document.body.appendChild(el);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
TextEditor.quill = new Quill("#textInnerEditor", {
|
|
29
|
+
theme: null as any,
|
|
30
|
+
modules: {
|
|
31
|
+
toolbar: false,
|
|
32
|
+
keyboard: {
|
|
33
|
+
bindings: {
|
|
34
|
+
enter: {
|
|
35
|
+
key: "Enter",
|
|
36
|
+
handler: (range: any, context: any) => {
|
|
37
|
+
const [line] = TextEditor.quill.getLine(range.index);
|
|
38
|
+
|
|
39
|
+
// 使用 Quill.import 获取 bubbleFormats
|
|
40
|
+
const BlockBlot = Quill.import("blots/block");
|
|
41
|
+
if (!BlockBlot || !(BlockBlot as any).bubbleFormats) return;
|
|
42
|
+
const lineFormats = (BlockBlot as any).bubbleFormats(line);
|
|
43
|
+
|
|
44
|
+
const delta = new Delta()
|
|
45
|
+
.retain(range.index)
|
|
46
|
+
.delete(range.length)
|
|
47
|
+
.insert("\n", lineFormats);
|
|
48
|
+
|
|
49
|
+
TextEditor.quill.updateContents(delta, Quill.sources.USER);
|
|
50
|
+
TextEditor.quill.setSelection(
|
|
51
|
+
range.index + 1,
|
|
52
|
+
Quill.sources.SILENT,
|
|
53
|
+
);
|
|
54
|
+
return false;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const FontAttributor: any = Quill.import("attributors/class/font");
|
|
63
|
+
FontAttributor.whitelist = [
|
|
64
|
+
"Roboto",
|
|
65
|
+
"RobotoMono",
|
|
66
|
+
"Inter",
|
|
67
|
+
"OpenSans",
|
|
68
|
+
"Montserrat",
|
|
69
|
+
"RobotoCondensed",
|
|
70
|
+
"Arimo",
|
|
71
|
+
"NotoSans",
|
|
72
|
+
"NotoSansSymbols",
|
|
73
|
+
"Merriweather",
|
|
74
|
+
"PlayfairDisplay",
|
|
75
|
+
"NotoSerif",
|
|
76
|
+
"Lato",
|
|
77
|
+
"Spectral",
|
|
78
|
+
"DancingScript",
|
|
79
|
+
"NotoSansSimplifiedChinese",
|
|
80
|
+
"NotoSerifSimplifiedChinese",
|
|
81
|
+
"NotoSansTraditionalChinese",
|
|
82
|
+
"NotoSansHongKong",
|
|
83
|
+
"NotoSerifTraditionalChinese",
|
|
84
|
+
"NotoSerifHongKong",
|
|
85
|
+
"NotoSansJapanese",
|
|
86
|
+
"NotoSansKorean",
|
|
87
|
+
"Poppins",
|
|
88
|
+
];
|
|
89
|
+
Quill.register(FontAttributor, true);
|
|
90
|
+
|
|
91
|
+
return TextEditor.quill;
|
|
92
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { TextEditor } from './TextEditor';
|
|
2
|
+
|
|
3
|
+
// 获取编辑器和quill实例的辅助函数
|
|
4
|
+
function getEditorContext() {
|
|
5
|
+
const quill = TextEditor.quill;
|
|
6
|
+
return { quill };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 更新text样式
|
|
10
|
+
export const updataHtmlText = async (e?: any, base64font?: any, fontObj?: any) => {
|
|
11
|
+
const { scaleX, scaleY } = e.worldTransform;
|
|
12
|
+
const zoomScale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
|
13
|
+
const dom: HTMLElement | null = document.querySelector('#textInnerEditor');
|
|
14
|
+
|
|
15
|
+
if (dom && e.data.textData?.fontFamily) {
|
|
16
|
+
dom.style.fontFamily = e.data.textData.fontFamily;
|
|
17
|
+
}
|
|
18
|
+
if (dom && e.data.textData?.fontSize) {
|
|
19
|
+
dom.style.fontSize = `${e.data.textData.fontSize * zoomScale}px`;
|
|
20
|
+
}
|
|
21
|
+
if (dom && e.data.textData?.lineHeight) {
|
|
22
|
+
dom.style.lineHeight = e.data.textData.lineHeight;
|
|
23
|
+
}
|
|
24
|
+
if (dom && e.data.textData?.letterSpacing) {
|
|
25
|
+
dom.style.letterSpacing = `${e.data.textData.letterSpacing}px`;
|
|
26
|
+
}
|
|
27
|
+
if (dom && e.data.textData?.textShadow) {
|
|
28
|
+
dom.style.textShadow = e.data.textData.textShadow;
|
|
29
|
+
} else if (dom) {
|
|
30
|
+
dom.style.textShadow = 'none';
|
|
31
|
+
}
|
|
32
|
+
if (dom && e.data.textData?.alignContent) {
|
|
33
|
+
const qlEditor: any = dom.querySelector('.ql-editor');
|
|
34
|
+
if (qlEditor) {
|
|
35
|
+
qlEditor.style.alignContent = e.data.textData.alignContent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { quill } = getEditorContext();
|
|
40
|
+
if (!quill) return;
|
|
41
|
+
|
|
42
|
+
// 如果是空的就把text置空
|
|
43
|
+
const html = quill.getSemanticHTML();
|
|
44
|
+
if (html === '<p></p>') {
|
|
45
|
+
if (e.text.includes('<style>@font-face')) {
|
|
46
|
+
e.text = e.text.split('</style>')[0] + '</style>';
|
|
47
|
+
} else {
|
|
48
|
+
e.text = '';
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (e.text.includes('<style>@font-face')) {
|
|
54
|
+
// text已经加载过字体
|
|
55
|
+
const style = e.text.split('</style>')[0];
|
|
56
|
+
if (fontObj && !style.includes(fontObj.code)) {
|
|
57
|
+
// 在字体style标签检索是否有当前字体 没有就添加字体style
|
|
58
|
+
const addStyle = `@font-face {font-family: ${fontObj.code};src: url(${base64font}) format('woff2') } .ql-font-${fontObj?.code?.replace(/\s+/g, '')} {font-family: ${fontObj.name};}`;
|
|
59
|
+
e.text =
|
|
60
|
+
style +
|
|
61
|
+
addStyle +
|
|
62
|
+
'</style>' +
|
|
63
|
+
addFontSizeToP(
|
|
64
|
+
e,
|
|
65
|
+
html,
|
|
66
|
+
e.data.textData.fontSize,
|
|
67
|
+
e.data.textData.lineHeight,
|
|
68
|
+
e.data.textData.letterSpacing,
|
|
69
|
+
e.data.textData.textShadow,
|
|
70
|
+
e.data.textData?.alignContent
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
// 样式当前字体已经存在
|
|
74
|
+
e.text =
|
|
75
|
+
style +
|
|
76
|
+
'</style>' +
|
|
77
|
+
addFontSizeToP(
|
|
78
|
+
e,
|
|
79
|
+
html,
|
|
80
|
+
e.data.textData.fontSize,
|
|
81
|
+
e.data.textData.lineHeight,
|
|
82
|
+
e.data.textData.letterSpacing,
|
|
83
|
+
e.data.textData.textShadow,
|
|
84
|
+
e.data.textData?.alignContent
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
} else if (base64font && fontObj) {
|
|
88
|
+
// 未加载过字体初次添加字体 存在base64font && fontObj才触发
|
|
89
|
+
const style = `<style>@font-face {font-family: ${e.data.textData.fontFamily.split(',')[0]};src: url(${base64font}) format('woff2') } .ql-font-${fontObj?.code?.replace(/\s+/g, '')} {font-family: ${fontObj.name};}</style>`;
|
|
90
|
+
e.text =
|
|
91
|
+
style +
|
|
92
|
+
addFontSizeToP(
|
|
93
|
+
e,
|
|
94
|
+
html,
|
|
95
|
+
e.data.textData.fontSize,
|
|
96
|
+
e.data.textData.lineHeight,
|
|
97
|
+
e.data.textData.letterSpacing,
|
|
98
|
+
e.data.textData.textShadow,
|
|
99
|
+
e.data.textData?.alignContent
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
// text没有包含字体就不加字体style
|
|
103
|
+
e.text = addFontSizeToP(
|
|
104
|
+
e,
|
|
105
|
+
html,
|
|
106
|
+
e.data.textData.fontSize,
|
|
107
|
+
e.data.textData.lineHeight,
|
|
108
|
+
e.data.textData.letterSpacing,
|
|
109
|
+
e.data.textData.textShadow,
|
|
110
|
+
e.data.textData?.alignContent
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const addFontSizeToP = (
|
|
116
|
+
e: any,
|
|
117
|
+
html: any,
|
|
118
|
+
fontSize = 16,
|
|
119
|
+
lineHeight = '1.5',
|
|
120
|
+
letterSpacing = '0',
|
|
121
|
+
textShadow = 'none',
|
|
122
|
+
alignContent = 'start'
|
|
123
|
+
) => {
|
|
124
|
+
const { quill } = getEditorContext();
|
|
125
|
+
if (!quill) return html;
|
|
126
|
+
|
|
127
|
+
const { scaleX, scaleY } = e.worldTransform;
|
|
128
|
+
const zoomScale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
|
129
|
+
const wrapper = document.createElement('div');
|
|
130
|
+
wrapper.innerHTML = html;
|
|
131
|
+
|
|
132
|
+
// 添加的样式 用于HTMLText渲染
|
|
133
|
+
const wrapperStyle: any = {
|
|
134
|
+
fontSize: `${fontSize}px`,
|
|
135
|
+
lineHeight,
|
|
136
|
+
letterSpacing: `${letterSpacing}px`,
|
|
137
|
+
textShadow
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
wrapper.querySelectorAll('p,ol,ul').forEach((p: any) => {
|
|
141
|
+
Object.assign(p.style, wrapperStyle);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 这个上下标还是要控制一下尺寸 因为html和canvas规范不一样 所以这里要控制一下
|
|
145
|
+
let str = wrapper.innerHTML;
|
|
146
|
+
|
|
147
|
+
// 排查如果有回车换行的加上一个字符
|
|
148
|
+
if (/<p\b[^>]*><\/p>/.test(str)) {
|
|
149
|
+
str = str.replace(/<p\b([^>]*)><\/p>/g, '<p$1> </p>');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let height;
|
|
153
|
+
const div: any = document.querySelector('#textInnerEditor');
|
|
154
|
+
|
|
155
|
+
// 获取包含自动换行的实际内容高度
|
|
156
|
+
const actualHeight = Number((quill.scroll.domNode.scrollHeight / zoomScale).toFixed(0));
|
|
157
|
+
const actualWidth = Number((quill.scroll.domNode.scrollWidth / zoomScale).toFixed(0));
|
|
158
|
+
|
|
159
|
+
if (['center', 'end'].includes(e.data.textData.alignContent)) {
|
|
160
|
+
if (e.parent.height < actualHeight) {
|
|
161
|
+
e.data.textData.alignContent = 'start';
|
|
162
|
+
height = (actualHeight || e.__layout.boxBounds.height) + 'px';
|
|
163
|
+
} else {
|
|
164
|
+
height = `${e.parent.height}px`;
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
if (e.parent.height < actualHeight) {
|
|
168
|
+
height = (actualHeight || e.__layout.boxBounds.height) + 'px';
|
|
169
|
+
} else {
|
|
170
|
+
height = `${e.parent.height}px`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 水平居中相关样式内嵌
|
|
175
|
+
const style = `<style>sub,sup{font-size:63%;}.ql-ui{position:absolute}ol,ul{counter-reset:list-0;padding-left:1.5em;margin:0}ol>li,ul>li{counter-increment:list-0;list-style-type:none;position:relative;padding-left:0;margin:0}ol>li::before{content:counter(list-0,decimal) '. ';position:absolute;left:-1.5em;width:1.2em;text-align:right}ul>li::before{content:'\\u2022';position:absolute;left:-1.5em;width:1.2em;text-align:right}li[data-list]{counter-set:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-align-center{text-align:center}.ql-align-right{text-align:right}.ql-align-left{text-align:left}.ql-align-justify{text-align:justify}</style>`;
|
|
176
|
+
|
|
177
|
+
let divBox = '';
|
|
178
|
+
if (e.parent.children[0].tag.includes('Shape')) {
|
|
179
|
+
divBox =
|
|
180
|
+
style +
|
|
181
|
+
`<div style="width: ${e.parent.width}px;height: ${e.parent.height}px;overflow-wrap:break-word;word-break:break-all;align-content:center;">${str}</div>`;
|
|
182
|
+
} else if (e.data.canChangeBox) {
|
|
183
|
+
divBox =
|
|
184
|
+
style +
|
|
185
|
+
`<div style="width: ${e.parent.width}px;height:${height};overflow-wrap:break-word;word-break:break-all;align-content:${alignContent};">${str}</div>`;
|
|
186
|
+
} else {
|
|
187
|
+
// 斜体补偿宽度 控制
|
|
188
|
+
if (e.data.textData.italic) {
|
|
189
|
+
divBox = style + str;
|
|
190
|
+
} else {
|
|
191
|
+
divBox = style + `<div style="width: ${actualWidth - 10}px">${str}</div>`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
console.log(divBox, '最终的html内容');
|
|
195
|
+
return divBox;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const setHTMLText = (
|
|
199
|
+
key: string,
|
|
200
|
+
value?: any,
|
|
201
|
+
base64font?: any,
|
|
202
|
+
editor?: any,
|
|
203
|
+
isInnerEditor?: boolean
|
|
204
|
+
) => {
|
|
205
|
+
const { quill } = getEditorContext();
|
|
206
|
+
if (!quill) {
|
|
207
|
+
console.error('Quill editor not initialized');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const rangeStr = localStorage.getItem('selection-change');
|
|
212
|
+
const range = rangeStr ? JSON.parse(rangeStr) : null;
|
|
213
|
+
|
|
214
|
+
if (range && isInnerEditor) {
|
|
215
|
+
quill.setSelection(range.index, range.length);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!editor || !editor.dateEdit) {
|
|
219
|
+
console.warn('Editor context not available for dateEdit');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
editor.dateEdit(async (e: any) => {
|
|
224
|
+
if (key === 'font') {
|
|
225
|
+
const fontSimpleName = value.code.replace(/\s+/g, '');
|
|
226
|
+
if (isInnerEditor) {
|
|
227
|
+
if (range && range.length) {
|
|
228
|
+
quill.formatText(range.index, range.length, key, fontSimpleName);
|
|
229
|
+
} else {
|
|
230
|
+
quill.formatText(0, quill.getLength() - 1, key, fontSimpleName);
|
|
231
|
+
}
|
|
232
|
+
updataHtmlText(e, base64font ?? null, value ?? null);
|
|
233
|
+
} else {
|
|
234
|
+
editor.isMultiSelect && editor.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
|
|
235
|
+
quill.formatText(0, quill.getLength() - 1, key, fontSimpleName);
|
|
236
|
+
updataHtmlText(e, base64font ?? null, value ?? null);
|
|
237
|
+
}
|
|
238
|
+
} else if (key === 'fontSize') {
|
|
239
|
+
e.data.textData[key] = value;
|
|
240
|
+
editor.isMultiSelect && editor.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
|
|
241
|
+
updataHtmlText(e, base64font ?? null);
|
|
242
|
+
} else if (key === 'textCase') {
|
|
243
|
+
if (isInnerEditor && range) {
|
|
244
|
+
const text = quill.getText(range.index, range.length);
|
|
245
|
+
const formats = quill.getFormat(range.index, range.length);
|
|
246
|
+
quill.deleteText(range.index, range.length);
|
|
247
|
+
let convertedText;
|
|
248
|
+
if (text === text.toUpperCase() && /[A-Z]/.test(text)) {
|
|
249
|
+
convertedText = text.toLowerCase();
|
|
250
|
+
} else if (text === text.toLowerCase() && /[a-z]/.test(text)) {
|
|
251
|
+
convertedText = text.toUpperCase();
|
|
252
|
+
} else {
|
|
253
|
+
convertedText = text.toUpperCase();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
quill.insertText(range.index, convertedText, formats);
|
|
257
|
+
if (range && isInnerEditor) {
|
|
258
|
+
quill.setSelection(range.index, range.length);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else if (key === 'script') {
|
|
262
|
+
let val = 'sub';
|
|
263
|
+
if (value === 'super') val = 'sup';
|
|
264
|
+
if (isInnerEditor) {
|
|
265
|
+
if (range && range.length) {
|
|
266
|
+
quill.formatText(range.index, range.length, key, quill.getFormat(range).script === value ? false : val);
|
|
267
|
+
} else {
|
|
268
|
+
quill.formatText(0, quill.getLength() - 1, key, quill.getFormat().script === value ? false : val);
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
editor.isMultiSelect && editor.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
|
|
272
|
+
quill.formatText(0, quill.getLength() - 1, key, quill.getFormat().script === value ? false : val);
|
|
273
|
+
updataHtmlText(e);
|
|
274
|
+
}
|
|
275
|
+
} else if (key === 'align') {
|
|
276
|
+
editor.isMultiSelect && editor.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
|
|
277
|
+
if (isInnerEditor) {
|
|
278
|
+
quill.format(key, value);
|
|
279
|
+
} else {
|
|
280
|
+
quill.formatLine(0, quill.getLength(), key, value);
|
|
281
|
+
}
|
|
282
|
+
updataHtmlText(e);
|
|
283
|
+
} else if (key === 'alignContent') {
|
|
284
|
+
editor.isMultiSelect && editor.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
|
|
285
|
+
e.data.textData[key] = value;
|
|
286
|
+
updataHtmlText(e);
|
|
287
|
+
} else if (key === 'color') {
|
|
288
|
+
editor.isMultiSelect && editor.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
|
|
289
|
+
quill.formatText(0, quill.getLength() - 1, key, value);
|
|
290
|
+
if (e.tag === 'HTMLText') {
|
|
291
|
+
updataHtmlText(e);
|
|
292
|
+
} else if (e.parent.findOne && e.parent.findOne('HTMLText')) {
|
|
293
|
+
updataHtmlText(e.parent.findOne('HTMLText'));
|
|
294
|
+
}
|
|
295
|
+
} else if (key === 'textShadow') {
|
|
296
|
+
editor.isMultiSelect && editor.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
|
|
297
|
+
e.data.textData[key] = value;
|
|
298
|
+
updataHtmlText(e);
|
|
299
|
+
} else if (key === 'list') {
|
|
300
|
+
editor.isMultiSelect && editor.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
|
|
301
|
+
if (isInnerEditor) {
|
|
302
|
+
const [line] = quill.getLine(range?.index || 0);
|
|
303
|
+
if (line.formats().list) {
|
|
304
|
+
quill.format(key, false);
|
|
305
|
+
} else {
|
|
306
|
+
quill.format(key, value);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
const [line] = quill.getLine(range?.index || 0);
|
|
310
|
+
if (line.formats().list) {
|
|
311
|
+
quill.formatLine(0, quill.getLength(), key, false);
|
|
312
|
+
} else {
|
|
313
|
+
quill.formatLine(0, quill.getLength(), key, value);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
updataHtmlText(e);
|
|
317
|
+
} else {
|
|
318
|
+
if (isInnerEditor) {
|
|
319
|
+
if (range && range.length) {
|
|
320
|
+
quill.formatText(range.index, range.length, key, !quill.getFormat(range)[key]);
|
|
321
|
+
} else {
|
|
322
|
+
quill.formatText(0, quill.getLength() - 1, key, !quill.getFormat()[key]);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
editor.isMultiSelect && editor.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
|
|
326
|
+
quill.formatText(0, quill.getLength() - 1, key, !quill.getFormat()[key]);
|
|
327
|
+
updataHtmlText(e);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}, 1);
|
|
331
|
+
};
|