f-docx-editor 0.1.2 → 0.1.4
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/FDocxEditor.common.js +17662 -0
- package/FDocxEditor.css +1 -0
- package/FDocxEditor.umd.js +17813 -0
- package/FDocxEditor.umd.min.js +1 -0
- package/package.json +52 -100
- package/dist/FDocxEditor.common.js +0 -199743
- package/dist/FDocxEditor.css +0 -1
- package/dist/FDocxEditor.umd.js +0 -199762
- package/dist/FDocxEditor.umd.min.js +0 -16
- package/dist/README.md +0 -112
- package/dist/demo.html +0 -1
- package/dist/package.json +0 -52
- package/src/App.vue +0 -116
- package/src/FDocxEditor.vue +0 -4613
- package/src/index.js +0 -8
- package/src/main.js +0 -8
- package/src/mock.js +0 -2408
package/src/FDocxEditor.vue
DELETED
|
@@ -1,4613 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="view-page" :class="{ 'has-outline': documentOutline.length }">
|
|
3
|
-
<aside v-if="documentOutline.length" class="document-outline">
|
|
4
|
-
<div class="outline-title">文档目录</div>
|
|
5
|
-
<button
|
|
6
|
-
v-for="item in documentOutline"
|
|
7
|
-
:key="item.id"
|
|
8
|
-
type="button"
|
|
9
|
-
class="outline-item"
|
|
10
|
-
:class="[`level-${item.level}`, { 'is-active': activeOutlineId === item.id }]"
|
|
11
|
-
@click="scrollToOutlineItem(item)"
|
|
12
|
-
>
|
|
13
|
-
<span class="outline-text">{{ item.text }}</span>
|
|
14
|
-
</button>
|
|
15
|
-
</aside>
|
|
16
|
-
|
|
17
|
-
<div
|
|
18
|
-
ref="docxScrollRef"
|
|
19
|
-
:class="renderType === 'view' ? 'view-box' : 'edit-box'"
|
|
20
|
-
@scroll="handleDocumentScroll"
|
|
21
|
-
>
|
|
22
|
-
<div v-if="isLoading" class="status-box">正在生成文档预览...</div>
|
|
23
|
-
<div v-else-if="errorMessage" class="status-box is-error">{{ errorMessage }}</div>
|
|
24
|
-
<div
|
|
25
|
-
v-show="!isLoading && !errorMessage"
|
|
26
|
-
ref="previewRef"
|
|
27
|
-
class="docx-preview-container"
|
|
28
|
-
></div>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<el-dialog
|
|
32
|
-
v-model="editDialogVisible"
|
|
33
|
-
title="编辑变量内容"
|
|
34
|
-
width="70%"
|
|
35
|
-
class="editDialog-dialog"
|
|
36
|
-
destroy-on-close
|
|
37
|
-
@closed="onDialogClosed"
|
|
38
|
-
>
|
|
39
|
-
<div class="edit-dialog">
|
|
40
|
-
<div class="edit-dialog-tip">
|
|
41
|
-
当前变量:{{ activeEditableField?.key || '--' }}
|
|
42
|
-
<span v-if="activeEditableField?.path" class="edit-dialog-path">
|
|
43
|
-
{{ activeEditableField.path }}
|
|
44
|
-
</span>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<div class="editor-toolbar">
|
|
48
|
-
<el-tooltip
|
|
49
|
-
v-for="item in headingActions"
|
|
50
|
-
:key="item.level"
|
|
51
|
-
:content="`标题 ${item.level}`"
|
|
52
|
-
placement="top"
|
|
53
|
-
>
|
|
54
|
-
<button
|
|
55
|
-
type="button"
|
|
56
|
-
class="toolbar-btn"
|
|
57
|
-
:class="{ 'is-active': isHeadingActive(item.level) }"
|
|
58
|
-
@mousedown.prevent
|
|
59
|
-
@click="setHeading(item.level)"
|
|
60
|
-
>
|
|
61
|
-
<svg class="toolbar-svg heading-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
62
|
-
<text x="5" y="19" class="svg-heading-main">H</text>
|
|
63
|
-
<text x="18" y="24" class="svg-heading-level">{{ item.level }}</text>
|
|
64
|
-
</svg>
|
|
65
|
-
</button>
|
|
66
|
-
</el-tooltip>
|
|
67
|
-
|
|
68
|
-
<el-tooltip content="正文" placement="top">
|
|
69
|
-
<button
|
|
70
|
-
type="button"
|
|
71
|
-
class="toolbar-btn"
|
|
72
|
-
:class="{ 'is-active': isParagraphActive }"
|
|
73
|
-
@mousedown.prevent
|
|
74
|
-
@click="setParagraph"
|
|
75
|
-
>
|
|
76
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
77
|
-
<path d="M6 7h16M6 12h16M6 17h13M6 22h10" class="svg-stroke round" />
|
|
78
|
-
</svg>
|
|
79
|
-
</button>
|
|
80
|
-
</el-tooltip>
|
|
81
|
-
|
|
82
|
-
<div class="toolbar-divider"></div>
|
|
83
|
-
|
|
84
|
-
<el-tooltip content="加粗" placement="top">
|
|
85
|
-
<button
|
|
86
|
-
type="button"
|
|
87
|
-
class="toolbar-btn"
|
|
88
|
-
:class="{ 'is-active': isBoldActive }"
|
|
89
|
-
@mousedown.prevent
|
|
90
|
-
@click="toggleBold"
|
|
91
|
-
>
|
|
92
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
93
|
-
<text x="8" y="20" class="svg-bold">B</text>
|
|
94
|
-
</svg>
|
|
95
|
-
</button>
|
|
96
|
-
</el-tooltip>
|
|
97
|
-
|
|
98
|
-
<el-popover
|
|
99
|
-
v-model:visible="tablePickerVisible"
|
|
100
|
-
placement="bottom-start"
|
|
101
|
-
trigger="click"
|
|
102
|
-
:width="340"
|
|
103
|
-
popper-class="table-picker-popper"
|
|
104
|
-
>
|
|
105
|
-
<template #reference>
|
|
106
|
-
<button
|
|
107
|
-
type="button"
|
|
108
|
-
class="toolbar-btn table-trigger-btn"
|
|
109
|
-
:class="{ 'is-active': isTableActive }"
|
|
110
|
-
title="插入表格"
|
|
111
|
-
@mousedown.prevent
|
|
112
|
-
>
|
|
113
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
114
|
-
<rect x="5" y="5" width="18" height="18" rx="2" class="svg-stroke" />
|
|
115
|
-
<path d="M5 11.5h18M5 17.5h18M11.5 5v18M17.5 5v18" class="svg-stroke thin" />
|
|
116
|
-
<rect x="6.5" y="6.5" width="15" height="4" class="svg-soft-fill" />
|
|
117
|
-
</svg>
|
|
118
|
-
</button>
|
|
119
|
-
</template>
|
|
120
|
-
|
|
121
|
-
<div class="table-picker">
|
|
122
|
-
<div class="table-picker-title">插入表格</div>
|
|
123
|
-
<div class="table-picker-grid">
|
|
124
|
-
<button
|
|
125
|
-
v-for="cell in tablePickerCells"
|
|
126
|
-
:key="cell.key"
|
|
127
|
-
type="button"
|
|
128
|
-
class="table-picker-cell"
|
|
129
|
-
:class="{ 'is-active': cell.row <= tablePickerHover.rows && cell.col <= tablePickerHover.cols }"
|
|
130
|
-
@mouseenter="onTablePickerHover(cell.row, cell.col)"
|
|
131
|
-
@click="insertTable(cell.row, cell.col)"
|
|
132
|
-
></button>
|
|
133
|
-
</div>
|
|
134
|
-
<div class="table-picker-tip">
|
|
135
|
-
{{ tablePickerHover.rows }} x {{ tablePickerHover.cols }}
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
</el-popover>
|
|
139
|
-
|
|
140
|
-
<el-tooltip content="左对齐" placement="top">
|
|
141
|
-
<button
|
|
142
|
-
type="button"
|
|
143
|
-
class="toolbar-btn"
|
|
144
|
-
:class="{ 'is-active': isTextAlignActive('left') }"
|
|
145
|
-
@mousedown.prevent
|
|
146
|
-
@click="setTextAlign('left')"
|
|
147
|
-
>
|
|
148
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
149
|
-
<path d="M6 7h16M6 12h11M6 17h16M6 22h9" class="svg-stroke round" />
|
|
150
|
-
</svg>
|
|
151
|
-
</button>
|
|
152
|
-
</el-tooltip>
|
|
153
|
-
|
|
154
|
-
<el-tooltip content="居中对齐" placement="top">
|
|
155
|
-
<button
|
|
156
|
-
type="button"
|
|
157
|
-
class="toolbar-btn"
|
|
158
|
-
:class="{ 'is-active': isTextAlignActive('center') }"
|
|
159
|
-
@mousedown.prevent
|
|
160
|
-
@click="setTextAlign('center')"
|
|
161
|
-
>
|
|
162
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
163
|
-
<path d="M6 7h16M9 12h10M6 17h16M10 22h8" class="svg-stroke round" />
|
|
164
|
-
</svg>
|
|
165
|
-
</button>
|
|
166
|
-
</el-tooltip>
|
|
167
|
-
|
|
168
|
-
<el-tooltip content="右对齐" placement="top">
|
|
169
|
-
<button
|
|
170
|
-
type="button"
|
|
171
|
-
class="toolbar-btn"
|
|
172
|
-
:class="{ 'is-active': isTextAlignActive('right') }"
|
|
173
|
-
@mousedown.prevent
|
|
174
|
-
@click="setTextAlign('right')"
|
|
175
|
-
>
|
|
176
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
177
|
-
<path d="M6 7h16M11 12h11M6 17h16M13 22h9" class="svg-stroke round" />
|
|
178
|
-
</svg>
|
|
179
|
-
</button>
|
|
180
|
-
</el-tooltip>
|
|
181
|
-
|
|
182
|
-
<div class="toolbar-divider"></div>
|
|
183
|
-
|
|
184
|
-
<el-tooltip content="下方插入行" placement="top">
|
|
185
|
-
<span class="toolbar-tip-wrap">
|
|
186
|
-
<button
|
|
187
|
-
type="button"
|
|
188
|
-
class="toolbar-btn table-op-btn"
|
|
189
|
-
:disabled="!isTableActive"
|
|
190
|
-
@mousedown.prevent
|
|
191
|
-
@click="addTableRow"
|
|
192
|
-
>
|
|
193
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
194
|
-
<rect x="5" y="4" width="18" height="16" rx="1.5" class="svg-stroke muted" />
|
|
195
|
-
<path d="M5 9.5h18M5 14.5h18M11 4v16M17 4v16" class="svg-stroke thin muted" />
|
|
196
|
-
<path d="M14 23v4M12 25h4" class="svg-stroke accent round" />
|
|
197
|
-
</svg>
|
|
198
|
-
</button>
|
|
199
|
-
</span>
|
|
200
|
-
</el-tooltip>
|
|
201
|
-
|
|
202
|
-
<el-tooltip content="删除当前行" placement="top">
|
|
203
|
-
<span class="toolbar-tip-wrap">
|
|
204
|
-
<button
|
|
205
|
-
type="button"
|
|
206
|
-
class="toolbar-btn table-op-btn"
|
|
207
|
-
:disabled="!isTableActive"
|
|
208
|
-
@mousedown.prevent
|
|
209
|
-
@click="deleteTableRow"
|
|
210
|
-
>
|
|
211
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
212
|
-
<rect x="5" y="4" width="18" height="16" rx="1.5" class="svg-stroke muted" />
|
|
213
|
-
<path d="M5 9.5h18M5 14.5h18M11 4v16M17 4v16" class="svg-stroke thin muted" />
|
|
214
|
-
<path d="M11.5 25h5" class="svg-stroke danger round" />
|
|
215
|
-
</svg>
|
|
216
|
-
</button>
|
|
217
|
-
</span>
|
|
218
|
-
</el-tooltip>
|
|
219
|
-
|
|
220
|
-
<el-tooltip content="右侧插入列" placement="top">
|
|
221
|
-
<span class="toolbar-tip-wrap">
|
|
222
|
-
<button
|
|
223
|
-
type="button"
|
|
224
|
-
class="toolbar-btn table-op-btn"
|
|
225
|
-
:disabled="!isTableActive"
|
|
226
|
-
@mousedown.prevent
|
|
227
|
-
@click="addTableColumn"
|
|
228
|
-
>
|
|
229
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
230
|
-
<rect x="4" y="5" width="16" height="18" rx="1.5" class="svg-stroke muted" />
|
|
231
|
-
<path d="M4 11h16M4 17h16M9.5 5v18M15 5v18" class="svg-stroke thin muted" />
|
|
232
|
-
<path d="M24 12v4M22 14h4" class="svg-stroke accent round" />
|
|
233
|
-
</svg>
|
|
234
|
-
</button>
|
|
235
|
-
</span>
|
|
236
|
-
</el-tooltip>
|
|
237
|
-
|
|
238
|
-
<el-tooltip content="删除当前列" placement="top">
|
|
239
|
-
<span class="toolbar-tip-wrap">
|
|
240
|
-
<button
|
|
241
|
-
type="button"
|
|
242
|
-
class="toolbar-btn table-op-btn"
|
|
243
|
-
:disabled="!isTableActive"
|
|
244
|
-
@mousedown.prevent
|
|
245
|
-
@click="deleteTableColumn"
|
|
246
|
-
>
|
|
247
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
248
|
-
<rect x="4" y="5" width="16" height="18" rx="1.5" class="svg-stroke muted" />
|
|
249
|
-
<path d="M4 11h16M4 17h16M9.5 5v18M15 5v18" class="svg-stroke thin muted" />
|
|
250
|
-
<path d="M22 14h5" class="svg-stroke danger round" />
|
|
251
|
-
</svg>
|
|
252
|
-
</button>
|
|
253
|
-
</span>
|
|
254
|
-
</el-tooltip>
|
|
255
|
-
|
|
256
|
-
<el-tooltip content="删除表格" placement="top">
|
|
257
|
-
<span class="toolbar-tip-wrap">
|
|
258
|
-
<button
|
|
259
|
-
type="button"
|
|
260
|
-
class="toolbar-btn table-op-btn"
|
|
261
|
-
:disabled="!isTableActive"
|
|
262
|
-
@mousedown.prevent
|
|
263
|
-
@click="removeTable"
|
|
264
|
-
>
|
|
265
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
266
|
-
<rect x="6" y="6" width="16" height="16" rx="2" class="svg-stroke muted" />
|
|
267
|
-
<path d="M11 11l6 6M17 11l-6 6" class="svg-stroke danger round" />
|
|
268
|
-
</svg>
|
|
269
|
-
</button>
|
|
270
|
-
</span>
|
|
271
|
-
</el-tooltip>
|
|
272
|
-
|
|
273
|
-
<div class="toolbar-divider"></div>
|
|
274
|
-
|
|
275
|
-
<el-tooltip content="字体颜色" placement="top">
|
|
276
|
-
<button type="button" class="toolbar-btn color-btn" @mousedown.prevent @click="openTextColorPicker">
|
|
277
|
-
<svg class="toolbar-svg color-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
278
|
-
<path d="M8 19l6-14 6 14M10.5 14.5h7" class="svg-stroke round" />
|
|
279
|
-
<path d="M6 23h16" class="svg-stroke color-preview-stroke round" :style="{ color: currentTextColor }" />
|
|
280
|
-
</svg>
|
|
281
|
-
</button>
|
|
282
|
-
</el-tooltip>
|
|
283
|
-
<input
|
|
284
|
-
ref="textColorInputRef"
|
|
285
|
-
class="hidden-color-input"
|
|
286
|
-
type="color"
|
|
287
|
-
:value="currentTextColor"
|
|
288
|
-
@input="onTextColorChange"
|
|
289
|
-
/>
|
|
290
|
-
|
|
291
|
-
<el-tooltip content="背景色" placement="top">
|
|
292
|
-
<button type="button" class="toolbar-btn color-btn" @mousedown.prevent @click="openHighlightColorPicker">
|
|
293
|
-
<svg class="toolbar-svg color-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
294
|
-
<path d="M8 16.5l7.5-7.5 3.5 3.5-7.5 7.5H8v-3.5z" class="svg-stroke round" />
|
|
295
|
-
<path d="M17 7l4 4" class="svg-stroke thin round" />
|
|
296
|
-
<rect x="6" y="22" width="16" height="3" rx="1.5" class="svg-color-fill" :style="{ color: currentHighlightColor }" />
|
|
297
|
-
</svg>
|
|
298
|
-
</button>
|
|
299
|
-
</el-tooltip>
|
|
300
|
-
|
|
301
|
-
<el-tooltip content="清除背景色" placement="top">
|
|
302
|
-
<button
|
|
303
|
-
type="button"
|
|
304
|
-
class="toolbar-btn clear-highlight-btn"
|
|
305
|
-
@mousedown.prevent
|
|
306
|
-
@click="clearHighlightColor"
|
|
307
|
-
>
|
|
308
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
309
|
-
<path d="M8 16.5l7.5-7.5 3.5 3.5-7.5 7.5H8v-3.5z" class="svg-stroke muted round" />
|
|
310
|
-
<path d="M17 7l4 4" class="svg-stroke thin muted round" />
|
|
311
|
-
<path d="M7 23h14" class="svg-stroke danger round" />
|
|
312
|
-
<path d="M21 6L6 21" class="svg-stroke danger round" />
|
|
313
|
-
</svg>
|
|
314
|
-
</button>
|
|
315
|
-
</el-tooltip>
|
|
316
|
-
|
|
317
|
-
<el-tooltip content="格式刷" placement="top">
|
|
318
|
-
<button
|
|
319
|
-
type="button"
|
|
320
|
-
class="toolbar-btn"
|
|
321
|
-
:class="{ 'is-active': Boolean(copiedFormat) }"
|
|
322
|
-
@mousedown.prevent
|
|
323
|
-
@click="toggleFormatPainter"
|
|
324
|
-
>
|
|
325
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
326
|
-
<path d="M8 5h12v6H8zM10 11v4h8v-4M13 15h2v7" class="svg-stroke round" />
|
|
327
|
-
</svg>
|
|
328
|
-
</button>
|
|
329
|
-
</el-tooltip>
|
|
330
|
-
|
|
331
|
-
<el-tooltip content="清除格式" placement="top">
|
|
332
|
-
<button
|
|
333
|
-
type="button"
|
|
334
|
-
class="toolbar-btn"
|
|
335
|
-
@mousedown.prevent
|
|
336
|
-
@click="clearEditorFormat"
|
|
337
|
-
>
|
|
338
|
-
<svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
|
|
339
|
-
<path d="M7 20l5-12h4l5 12M9 16h10" class="svg-stroke round" />
|
|
340
|
-
<path d="M21 6L6 21" class="svg-stroke danger round" />
|
|
341
|
-
</svg>
|
|
342
|
-
</button>
|
|
343
|
-
</el-tooltip>
|
|
344
|
-
<input
|
|
345
|
-
ref="highlightColorInputRef"
|
|
346
|
-
class="hidden-color-input"
|
|
347
|
-
type="color"
|
|
348
|
-
:value="currentHighlightColor"
|
|
349
|
-
@input="onHighlightColorChange"
|
|
350
|
-
/>
|
|
351
|
-
|
|
352
|
-
<div class="toolbar-divider"></div>
|
|
353
|
-
|
|
354
|
-
<el-tooltip content="撤销" placement="top">
|
|
355
|
-
<span class="toolbar-tip-wrap">
|
|
356
|
-
<button
|
|
357
|
-
type="button"
|
|
358
|
-
class="toolbar-btn"
|
|
359
|
-
:disabled="!canUndo"
|
|
360
|
-
@mousedown.prevent
|
|
361
|
-
@click="undoEditor"
|
|
362
|
-
>
|
|
363
|
-
<RefreshLeft />
|
|
364
|
-
</button>
|
|
365
|
-
</span>
|
|
366
|
-
</el-tooltip>
|
|
367
|
-
|
|
368
|
-
<el-tooltip content="恢复" placement="top">
|
|
369
|
-
<span class="toolbar-tip-wrap">
|
|
370
|
-
<button
|
|
371
|
-
type="button"
|
|
372
|
-
class="toolbar-btn"
|
|
373
|
-
:disabled="!canRedo"
|
|
374
|
-
@mousedown.prevent
|
|
375
|
-
@click="redoEditor"
|
|
376
|
-
>
|
|
377
|
-
<RefreshRight />
|
|
378
|
-
</button>
|
|
379
|
-
</span>
|
|
380
|
-
</el-tooltip>
|
|
381
|
-
</div>
|
|
382
|
-
|
|
383
|
-
<div
|
|
384
|
-
class="editor-content-shell"
|
|
385
|
-
@click="hideTableContextMenu"
|
|
386
|
-
@mousedown.capture="onEditorMouseDownCapture"
|
|
387
|
-
@contextmenu="onEditorContextMenu"
|
|
388
|
-
>
|
|
389
|
-
<EditorContent v-if="editor" :editor="editor" class="editor-content" />
|
|
390
|
-
</div>
|
|
391
|
-
|
|
392
|
-
<div
|
|
393
|
-
v-if="tableContextMenuVisible"
|
|
394
|
-
class="table-context-menu"
|
|
395
|
-
:style="{ left: `${tableContextMenuPosition.x}px`, top: `${tableContextMenuPosition.y}px` }"
|
|
396
|
-
@mousedown.prevent
|
|
397
|
-
>
|
|
398
|
-
<button type="button" @click="runTableMenuCommand('mergeCells')">合并单元格</button>
|
|
399
|
-
<button type="button" @click="runTableMenuCommand('splitCell')">拆分单元格</button>
|
|
400
|
-
<div class="table-context-divider"></div>
|
|
401
|
-
<button type="button" @click="runTableMenuCommand('addRowBefore')">上方插入行</button>
|
|
402
|
-
<button type="button" @click="runTableMenuCommand('addRowAfter')">下方插入行</button>
|
|
403
|
-
<button type="button" @click="runTableMenuCommand('deleteRow')">删除当前行</button>
|
|
404
|
-
<div class="table-context-divider"></div>
|
|
405
|
-
<button type="button" @click="runTableMenuCommand('addColumnBefore')">左侧插入列</button>
|
|
406
|
-
<button type="button" @click="runTableMenuCommand('addColumnAfter')">右侧插入列</button>
|
|
407
|
-
<button type="button" @click="runTableMenuCommand('deleteColumn')">删除当前列</button>
|
|
408
|
-
<div class="table-context-divider"></div>
|
|
409
|
-
<button type="button" @click="runTableMenuCommand('toggleHeaderRow')">切换表头行</button>
|
|
410
|
-
<button type="button" @click="runTableMenuCommand('toggleHeaderColumn')">切换表头列</button>
|
|
411
|
-
<button type="button" class="is-danger" @click="runTableMenuCommand('deleteTable')">删除表格</button>
|
|
412
|
-
</div>
|
|
413
|
-
</div>
|
|
414
|
-
|
|
415
|
-
<template #footer>
|
|
416
|
-
<div class="dialog-footer">
|
|
417
|
-
<el-button @click="editDialogVisible = false">取消</el-button>
|
|
418
|
-
<el-button type="primary" @click="saveActiveField">保存当前内容</el-button>
|
|
419
|
-
</div>
|
|
420
|
-
</template>
|
|
421
|
-
</el-dialog>
|
|
422
|
-
|
|
423
|
-
</div>
|
|
424
|
-
</template>
|
|
425
|
-
|
|
426
|
-
<script>
|
|
427
|
-
export default {
|
|
428
|
-
name: "DocxEditor"
|
|
429
|
-
}
|
|
430
|
-
</script>
|
|
431
|
-
|
|
432
|
-
<script setup>
|
|
433
|
-
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
|
434
|
-
import { ElMessage } from 'element-plus'
|
|
435
|
-
import {
|
|
436
|
-
RefreshLeft,
|
|
437
|
-
RefreshRight
|
|
438
|
-
} from '@element-plus/icons-vue'
|
|
439
|
-
import Docxtemplater from 'docxtemplater'
|
|
440
|
-
import ImageModule from 'docxtemplater-image-module-free'
|
|
441
|
-
import PizZip from 'pizzip'
|
|
442
|
-
import * as docxPreview from 'docx-preview'
|
|
443
|
-
import * as echarts from 'echarts'
|
|
444
|
-
import { Extension } from '@tiptap/core'
|
|
445
|
-
import Color from '@tiptap/extension-color'
|
|
446
|
-
import Highlight from '@tiptap/extension-highlight'
|
|
447
|
-
import Placeholder from '@tiptap/extension-placeholder'
|
|
448
|
-
import { CellSelection } from '@tiptap/pm/tables'
|
|
449
|
-
import { Table } from '@tiptap/extension-table'
|
|
450
|
-
import TableCell from '@tiptap/extension-table-cell'
|
|
451
|
-
import TableHeader from '@tiptap/extension-table-header'
|
|
452
|
-
import TableRow from '@tiptap/extension-table-row'
|
|
453
|
-
import TextAlign from '@tiptap/extension-text-align'
|
|
454
|
-
import { TextStyle } from '@tiptap/extension-text-style'
|
|
455
|
-
import StarterKit from '@tiptap/starter-kit'
|
|
456
|
-
import { EditorContent, useEditor } from '@tiptap/vue-3'
|
|
457
|
-
|
|
458
|
-
const DEFAULT_TEMPLATE_FILE_NAME = '运营报告.docx'
|
|
459
|
-
const DOCX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
460
|
-
const DEFAULT_TEXT_COLOR = '#2b3345'
|
|
461
|
-
const DEFAULT_HIGHLIGHT_COLOR = '#e8f3ff'
|
|
462
|
-
const EDITOR_MARKER_START = '[EDITOR_START]'
|
|
463
|
-
const EDITOR_MARKER_END = '[EDITOR_END]'
|
|
464
|
-
const LEGACY_EDITOR_MARKER_START = '【'
|
|
465
|
-
const LEGACY_EDITOR_MARKER_END = '】'
|
|
466
|
-
const WATER_LEVEL_CHART_TAGS = ['cpuWaterLevelChart', 'memoryWaterLevelChart', 'diskWaterLevelChart']
|
|
467
|
-
const WATER_LEVEL_CHART_CANVAS_WIDTH = 720
|
|
468
|
-
const WATER_LEVEL_CHART_CANVAS_HEIGHT = 360
|
|
469
|
-
const WATER_LEVEL_CHART_DOCX_WIDTH = 554
|
|
470
|
-
const WATER_LEVEL_CHART_DOCX_HEIGHT = 277
|
|
471
|
-
const WATER_LEVEL_CHART_GRID = {
|
|
472
|
-
top: 88,
|
|
473
|
-
left: 48,
|
|
474
|
-
right: 36,
|
|
475
|
-
bottom: 42
|
|
476
|
-
}
|
|
477
|
-
const DOCX_TABLE_WIDTH_DXA = 8306
|
|
478
|
-
const DOCX_TABLE_WIDTH_PCT = 5000
|
|
479
|
-
const DOCX_TABLE_FONT_NAME = '方正仿宋_GB2312'
|
|
480
|
-
const DOCX_TABLE_HEADER_FONT_SIZE_HALF_POINTS = '28'
|
|
481
|
-
const DOCX_TABLE_BODY_FONT_SIZE_HALF_POINTS = '22'
|
|
482
|
-
const EDITOR_TABLE_FONT_FAMILY = "'方正仿宋_GB2312', '仿宋_GB2312', FangSong, STFangsong, SimSun, '宋体', serif"
|
|
483
|
-
const EDITOR_TABLE_HEADER_FONT_SIZE = '14pt'
|
|
484
|
-
const EDITOR_TABLE_BODY_FONT_SIZE = '11pt'
|
|
485
|
-
const TABLE_PICKER_ROWS = 8
|
|
486
|
-
const TABLE_PICKER_COLS = 10
|
|
487
|
-
const RICH_TEXT_TOKEN_PREFIX = '__DOCX_RICH_TEXT_FIELD__'
|
|
488
|
-
const EDITABLE_EMPTY_PLACEHOLDER = '请输入内容...'
|
|
489
|
-
const EDITOR_EMPTY_PLACEHOLDER = '请输入内容,支持 Ctrl/Command + A/C/Z/Y 等常用快捷键...'
|
|
490
|
-
const EDITOR_INDENT_STEP_EM = 2
|
|
491
|
-
const EDITOR_MAX_INDENT_LEVEL = 6
|
|
492
|
-
|
|
493
|
-
const props = defineProps({
|
|
494
|
-
renderData: {
|
|
495
|
-
type: Object,
|
|
496
|
-
default: () => ({})
|
|
497
|
-
},
|
|
498
|
-
renderType: {
|
|
499
|
-
type: String,
|
|
500
|
-
default: 'view'
|
|
501
|
-
},
|
|
502
|
-
templateUrl: {
|
|
503
|
-
type: String,
|
|
504
|
-
default: ''
|
|
505
|
-
},
|
|
506
|
-
templateFileName: {
|
|
507
|
-
type: String,
|
|
508
|
-
default: '运营报告.docx'
|
|
509
|
-
},
|
|
510
|
-
exportFileName: {
|
|
511
|
-
type: String,
|
|
512
|
-
default: ''
|
|
513
|
-
}
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
const emit = defineEmits([
|
|
517
|
-
'content-change',
|
|
518
|
-
'render-data-change',
|
|
519
|
-
'save-payload-change',
|
|
520
|
-
'update:editorHtml',
|
|
521
|
-
'update:editorJson'
|
|
522
|
-
])
|
|
523
|
-
|
|
524
|
-
const previewRef = ref(null)
|
|
525
|
-
const docxScrollRef = ref(null)
|
|
526
|
-
const isLoading = ref(false)
|
|
527
|
-
const errorMessage = ref('')
|
|
528
|
-
const editDialogVisible = ref(false)
|
|
529
|
-
const activeEditableField = ref(null)
|
|
530
|
-
const textColorInputRef = ref(null)
|
|
531
|
-
const highlightColorInputRef = ref(null)
|
|
532
|
-
const currentTextColor = ref(DEFAULT_TEXT_COLOR)
|
|
533
|
-
const currentHighlightColor = ref(DEFAULT_HIGHLIGHT_COLOR)
|
|
534
|
-
const pendingEditorSelection = ref(null)
|
|
535
|
-
const copiedFormat = ref(null)
|
|
536
|
-
const tablePickerVisible = ref(false)
|
|
537
|
-
const tablePickerHover = ref({
|
|
538
|
-
rows: 3,
|
|
539
|
-
cols: 3
|
|
540
|
-
})
|
|
541
|
-
const tableContextMenuVisible = ref(false)
|
|
542
|
-
const tableContextMenuPosition = ref({
|
|
543
|
-
x: 0,
|
|
544
|
-
y: 0
|
|
545
|
-
})
|
|
546
|
-
const documentOutline = ref([])
|
|
547
|
-
const activeOutlineId = ref('')
|
|
548
|
-
const editableFieldKeys = ref([])
|
|
549
|
-
const editableMarkerDescriptors = ref([])
|
|
550
|
-
const editableFieldPathMap = ref({})
|
|
551
|
-
const editableFieldContentMap = reactive({})
|
|
552
|
-
const editableFieldTemplateHtmlMap = reactive({})
|
|
553
|
-
const headingStyleLevelMap = ref({})
|
|
554
|
-
const documentHeadingList = ref([])
|
|
555
|
-
const wordStyleContext = ref({
|
|
556
|
-
headingStyleIds: {},
|
|
557
|
-
bodyStyleId: '',
|
|
558
|
-
editableHeadingCounters: {}
|
|
559
|
-
})
|
|
560
|
-
const headingActions = [
|
|
561
|
-
{ label: 'H1', level: 1 },
|
|
562
|
-
{ label: 'H2', level: 2 },
|
|
563
|
-
{ label: 'H3', level: 3 }
|
|
564
|
-
]
|
|
565
|
-
const tablePickerCells = Array.from({ length: TABLE_PICKER_ROWS * TABLE_PICKER_COLS }, (_, index) => ({
|
|
566
|
-
key: index,
|
|
567
|
-
row: Math.floor(index / TABLE_PICKER_COLS) + 1,
|
|
568
|
-
col: (index % TABLE_PICKER_COLS) + 1
|
|
569
|
-
}))
|
|
570
|
-
|
|
571
|
-
let renderToken = 0
|
|
572
|
-
let editableDomCleanupList = []
|
|
573
|
-
let outlineScrollRaf = 0
|
|
574
|
-
const editableRenderData = ref(createEditableRenderData(props.renderData))
|
|
575
|
-
const DOM_TEXT_NODE = 3
|
|
576
|
-
const DOM_ELEMENT_NODE = 1
|
|
577
|
-
|
|
578
|
-
function getEditorIndentLevel(editorInstance) {
|
|
579
|
-
const attrs = editorInstance.isActive('heading')
|
|
580
|
-
? editorInstance.getAttributes('heading')
|
|
581
|
-
: editorInstance.getAttributes('paragraph')
|
|
582
|
-
|
|
583
|
-
return Number(attrs.textIndentLevel || 0)
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function updateEditorIndent(editorInstance, delta) {
|
|
587
|
-
if (!editorInstance || editorInstance.isActive('table')) {
|
|
588
|
-
return false
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const nodeType = editorInstance.isActive('heading') ? 'heading' : 'paragraph'
|
|
592
|
-
const nextLevel = Math.min(
|
|
593
|
-
EDITOR_MAX_INDENT_LEVEL,
|
|
594
|
-
Math.max(0, getEditorIndentLevel(editorInstance) + delta)
|
|
595
|
-
)
|
|
596
|
-
|
|
597
|
-
return editorInstance
|
|
598
|
-
.chain()
|
|
599
|
-
.focus()
|
|
600
|
-
.updateAttributes(nodeType, {
|
|
601
|
-
textIndentLevel: nextLevel
|
|
602
|
-
})
|
|
603
|
-
.run()
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const EditorIndent = Extension.create({
|
|
607
|
-
name: 'editorIndent',
|
|
608
|
-
|
|
609
|
-
addGlobalAttributes() {
|
|
610
|
-
return [
|
|
611
|
-
{
|
|
612
|
-
types: ['paragraph', 'heading'],
|
|
613
|
-
attributes: {
|
|
614
|
-
textIndentLevel: {
|
|
615
|
-
default: 0,
|
|
616
|
-
parseHTML: (element) => {
|
|
617
|
-
const indent = element.style?.textIndent || ''
|
|
618
|
-
const match = indent.match(/^([\d.]+)em$/)
|
|
619
|
-
|
|
620
|
-
if (!match) {
|
|
621
|
-
return 0
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
return Math.min(
|
|
625
|
-
EDITOR_MAX_INDENT_LEVEL,
|
|
626
|
-
Math.max(0, Math.round(Number(match[1]) / EDITOR_INDENT_STEP_EM))
|
|
627
|
-
)
|
|
628
|
-
},
|
|
629
|
-
renderHTML: (attributes) => {
|
|
630
|
-
const level = Number(attributes.textIndentLevel || 0)
|
|
631
|
-
|
|
632
|
-
if (!level) {
|
|
633
|
-
return {}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
return {
|
|
637
|
-
style: `text-indent: ${level * EDITOR_INDENT_STEP_EM}em;`
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
]
|
|
644
|
-
},
|
|
645
|
-
|
|
646
|
-
addKeyboardShortcuts() {
|
|
647
|
-
return {
|
|
648
|
-
Tab: () => updateEditorIndent(this.editor, 1),
|
|
649
|
-
'Shift-Tab': () => updateEditorIndent(this.editor, -1),
|
|
650
|
-
'Mod-y': () => this.editor.commands.redo(),
|
|
651
|
-
'Mod-Shift-z': () => this.editor.commands.redo(),
|
|
652
|
-
'Mod-z': () => this.editor.commands.undo()
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
const editor = useEditor({
|
|
658
|
-
content: '<p></p>',
|
|
659
|
-
extensions: [
|
|
660
|
-
StarterKit.configure({
|
|
661
|
-
bulletList: false,
|
|
662
|
-
orderedList: false,
|
|
663
|
-
blockquote: false,
|
|
664
|
-
codeBlock: false,
|
|
665
|
-
horizontalRule: false
|
|
666
|
-
}),
|
|
667
|
-
Table.configure({
|
|
668
|
-
resizable: true
|
|
669
|
-
}),
|
|
670
|
-
TableRow,
|
|
671
|
-
TableHeader,
|
|
672
|
-
TableCell,
|
|
673
|
-
TextStyle,
|
|
674
|
-
Color.configure({
|
|
675
|
-
types: ['textStyle']
|
|
676
|
-
}),
|
|
677
|
-
Highlight.configure({
|
|
678
|
-
multicolor: true
|
|
679
|
-
}),
|
|
680
|
-
TextAlign.configure({
|
|
681
|
-
types: ['heading', 'paragraph']
|
|
682
|
-
}),
|
|
683
|
-
EditorIndent,
|
|
684
|
-
Placeholder.configure({
|
|
685
|
-
placeholder: EDITOR_EMPTY_PLACEHOLDER,
|
|
686
|
-
showOnlyWhenEditable: true,
|
|
687
|
-
includeChildren: true
|
|
688
|
-
})
|
|
689
|
-
],
|
|
690
|
-
onSelectionUpdate({ editor: instance }) {
|
|
691
|
-
currentTextColor.value = instance.getAttributes('textStyle').color || DEFAULT_TEXT_COLOR
|
|
692
|
-
currentHighlightColor.value = instance.getAttributes('highlight').color || DEFAULT_HIGHLIGHT_COLOR
|
|
693
|
-
},
|
|
694
|
-
onUpdate({ editor: instance }) {
|
|
695
|
-
emit('content-change', {
|
|
696
|
-
html: instance.getHTML(),
|
|
697
|
-
json: instance.getJSON()
|
|
698
|
-
})
|
|
699
|
-
emit('update:editorHtml', instance.getHTML())
|
|
700
|
-
emit('update:editorJson', instance.getJSON())
|
|
701
|
-
}
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
const isBoldActive = computed(() => editor.value?.isActive('bold'))
|
|
705
|
-
const isTableActive = computed(() => editor.value?.isActive('table'))
|
|
706
|
-
const isParagraphActive = computed(() => editor.value?.isActive('paragraph'))
|
|
707
|
-
const canUndo = computed(() => editor.value?.can().chain().focus().undo().run())
|
|
708
|
-
const canRedo = computed(() => editor.value?.can().chain().focus().redo().run())
|
|
709
|
-
|
|
710
|
-
function deepClone(value) {
|
|
711
|
-
return JSON.parse(JSON.stringify(value || {}))
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function createEditableRenderData(source) {
|
|
715
|
-
const cloned = deepClone(source)
|
|
716
|
-
|
|
717
|
-
if (!cloned.__editableRichTextMap) {
|
|
718
|
-
cloned.__editableRichTextMap = {}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
return cloned
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function getChartTagDataAliases() {
|
|
725
|
-
return WATER_LEVEL_CHART_TAGS.reduce((result, key) => {
|
|
726
|
-
result[key] = getValueByPath(editableRenderData.value, `overallData.${key}`) || editableRenderData.value[key] || {}
|
|
727
|
-
return result
|
|
728
|
-
}, {})
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function getChartTagNames() {
|
|
732
|
-
return WATER_LEVEL_CHART_TAGS.reduce((result, key) => {
|
|
733
|
-
result[`{%${key}}`] = true
|
|
734
|
-
result[key] = true
|
|
735
|
-
return result
|
|
736
|
-
}, {})
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function getExportFileName() {
|
|
740
|
-
if (props.exportFileName) {
|
|
741
|
-
return props.exportFileName
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const startTime = editableRenderData.value?.startTime || ''
|
|
745
|
-
const endTime = editableRenderData.value?.endTime || ''
|
|
746
|
-
|
|
747
|
-
if (startTime && endTime) {
|
|
748
|
-
return `云平台运维周报_${startTime}_${endTime}.docx`
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
return '云平台运维周报.docx'
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function clearPreview() {
|
|
755
|
-
clearEditableDecorations()
|
|
756
|
-
documentOutline.value = []
|
|
757
|
-
activeOutlineId.value = ''
|
|
758
|
-
Object.keys(editableFieldTemplateHtmlMap).forEach((key) => {
|
|
759
|
-
delete editableFieldTemplateHtmlMap[key]
|
|
760
|
-
})
|
|
761
|
-
if (previewRef.value) {
|
|
762
|
-
previewRef.value.innerHTML = ''
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function clearEditableDecorations() {
|
|
767
|
-
editableDomCleanupList.forEach((cleanup) => cleanup())
|
|
768
|
-
editableDomCleanupList = []
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function normalizeValue(value) {
|
|
772
|
-
if (Array.isArray(value)) {
|
|
773
|
-
return value.map((item) => {
|
|
774
|
-
if (item && typeof item === 'object') {
|
|
775
|
-
return normalizeObject(item)
|
|
776
|
-
}
|
|
777
|
-
return item ?? ''
|
|
778
|
-
})
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (value && typeof value === 'object') {
|
|
782
|
-
return normalizeObject(value)
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
return value ?? ''
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function normalizeObject(source = {}) {
|
|
789
|
-
return Object.keys(source).reduce((result, key) => {
|
|
790
|
-
result[key] = normalizeValue(source[key])
|
|
791
|
-
return result
|
|
792
|
-
}, {})
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function buildTemplateData(data = {}) {
|
|
796
|
-
const normalizedData = normalizeObject(data)
|
|
797
|
-
const overallData = normalizedData.overallData || {}
|
|
798
|
-
const operationsData = normalizedData.operationsData || {}
|
|
799
|
-
const maintenanceData = normalizedData.maintenanceData || {}
|
|
800
|
-
const workThisWeek = normalizedData.workThisWeek || {}
|
|
801
|
-
const workNextWeek = normalizedData.workNextWeek || {}
|
|
802
|
-
|
|
803
|
-
return {
|
|
804
|
-
...normalizedData,
|
|
805
|
-
...overallData,
|
|
806
|
-
...operationsData,
|
|
807
|
-
...maintenanceData,
|
|
808
|
-
...workThisWeek,
|
|
809
|
-
...workNextWeek,
|
|
810
|
-
intranet_resource_list: overallData.intranet_resource_list || []
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function getRichTextToken(fieldKey) {
|
|
815
|
-
return `${RICH_TEXT_TOKEN_PREFIX}${fieldKey}__`
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function hasRichTextContent(richItem) {
|
|
819
|
-
return Boolean(richItem?.html || richItem?.json || richItem?.blocks?.length)
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
function getStoredRichTextItem(fieldKey) {
|
|
823
|
-
return editableRenderData.value.__editableRichTextMap?.[fieldKey] || null
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function buildTemplateDataForRender(options = {}) {
|
|
827
|
-
const { useRichTextTokens = true, useRichTextPlainText = true } = options
|
|
828
|
-
const source = deepClone(editableRenderData.value)
|
|
829
|
-
const richTextMap = source.__editableRichTextMap || {}
|
|
830
|
-
|
|
831
|
-
editableFieldKeys.value.forEach((fieldKey) => {
|
|
832
|
-
if (getValueByPath(source, fieldKey) === '') {
|
|
833
|
-
setValueByPath(source, fieldKey, '')
|
|
834
|
-
}
|
|
835
|
-
})
|
|
836
|
-
|
|
837
|
-
Object.keys(richTextMap).forEach((key) => {
|
|
838
|
-
const richItem = richTextMap[key]
|
|
839
|
-
if (richItem?.path) {
|
|
840
|
-
const shouldUseToken = useRichTextTokens && hasRichTextContent(richItem)
|
|
841
|
-
const fallbackText = useRichTextPlainText ? (richItem.plainText || '') : ''
|
|
842
|
-
setValueByPath(source, richItem.path, shouldUseToken
|
|
843
|
-
? getRichTextToken(key)
|
|
844
|
-
: fallbackText)
|
|
845
|
-
}
|
|
846
|
-
})
|
|
847
|
-
|
|
848
|
-
return {
|
|
849
|
-
...buildTemplateData(source),
|
|
850
|
-
...getChartTagDataAliases()
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function getTemplateTextParts(xmlText) {
|
|
855
|
-
return [...xmlText.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/g)].map((match) => decodeXmlText(match[1]))
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
function escapeRegExp(text) {
|
|
859
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function getEditableMarkerRegExp() {
|
|
863
|
-
return new RegExp(
|
|
864
|
-
`(?:${escapeRegExp(LEGACY_EDITOR_MARKER_START)}\\s*)?${escapeRegExp(EDITOR_MARKER_START)}\\s*(\\{([^#/{][^}]*)\\})[\\s\\S]*?${escapeRegExp(EDITOR_MARKER_END)}(?:\\s*${escapeRegExp(LEGACY_EDITOR_MARKER_END)})?`,
|
|
865
|
-
'g'
|
|
866
|
-
)
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function getRenderedEditableMarkerRegExp() {
|
|
870
|
-
return new RegExp(
|
|
871
|
-
`(?:${escapeRegExp(LEGACY_EDITOR_MARKER_START)}\\s*)?${escapeRegExp(EDITOR_MARKER_START)}\\s*([\\s\\S]*?)\\s*${escapeRegExp(EDITOR_MARKER_END)}(?:\\s*${escapeRegExp(LEGACY_EDITOR_MARKER_END)})?`,
|
|
872
|
-
'g'
|
|
873
|
-
)
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function decodeXmlText(text) {
|
|
877
|
-
return text
|
|
878
|
-
.replace(/</g, '<')
|
|
879
|
-
.replace(/>/g, '>')
|
|
880
|
-
.replace(/&/g, '&')
|
|
881
|
-
.replace(/"/g, '"')
|
|
882
|
-
.replace(/'/g, "'")
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
function extractEditableMarkerDescriptors(xmlText) {
|
|
886
|
-
const text = getTemplateTextParts(xmlText).join('')
|
|
887
|
-
const validMarkerRegExp = getEditableMarkerRegExp()
|
|
888
|
-
|
|
889
|
-
return [...text.matchAll(getRenderedEditableMarkerRegExp())]
|
|
890
|
-
.map((match) => {
|
|
891
|
-
const markerText = match[0] || ''
|
|
892
|
-
const validMatch = validMarkerRegExp.exec(markerText)
|
|
893
|
-
validMarkerRegExp.lastIndex = 0
|
|
894
|
-
|
|
895
|
-
return {
|
|
896
|
-
markerText,
|
|
897
|
-
fieldKey: validMatch?.[2] || '',
|
|
898
|
-
isValid: Boolean(validMatch?.[2])
|
|
899
|
-
}
|
|
900
|
-
})
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function buildScalarPathMap(source, currentPath = '', result = {}) {
|
|
904
|
-
if (!source || typeof source !== 'object') {
|
|
905
|
-
return result
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
Object.keys(source).forEach((key) => {
|
|
909
|
-
if (key.startsWith('__')) {
|
|
910
|
-
return
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
const value = source[key]
|
|
914
|
-
const nextPath = currentPath ? `${currentPath}.${key}` : key
|
|
915
|
-
|
|
916
|
-
if (Array.isArray(value)) {
|
|
917
|
-
return
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
if (value && typeof value === 'object') {
|
|
921
|
-
buildScalarPathMap(value, nextPath, result)
|
|
922
|
-
return
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
if (!result[key]) {
|
|
926
|
-
result[key] = nextPath
|
|
927
|
-
}
|
|
928
|
-
})
|
|
929
|
-
|
|
930
|
-
return result
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
function setValueByPath(target, path, value) {
|
|
934
|
-
if (!path) {
|
|
935
|
-
return
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
const pathList = path.split('.')
|
|
939
|
-
let current = target
|
|
940
|
-
|
|
941
|
-
for (let index = 0; index < pathList.length - 1; index += 1) {
|
|
942
|
-
const pathKey = pathList[index]
|
|
943
|
-
if (!current[pathKey] || typeof current[pathKey] !== 'object') {
|
|
944
|
-
current[pathKey] = {}
|
|
945
|
-
}
|
|
946
|
-
current = current[pathKey]
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
current[pathList[pathList.length - 1]] = value
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function getValueByPath(target, path) {
|
|
953
|
-
if (!path) {
|
|
954
|
-
return ''
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
return path.split('.').reduce((result, key) => {
|
|
958
|
-
if (result == null) {
|
|
959
|
-
return ''
|
|
960
|
-
}
|
|
961
|
-
return result[key]
|
|
962
|
-
}, target)
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function htmlToDocxText(html) {
|
|
966
|
-
const container = document.createElement('div')
|
|
967
|
-
container.innerHTML = html
|
|
968
|
-
|
|
969
|
-
function walk(node) {
|
|
970
|
-
if (!node) {
|
|
971
|
-
return ''
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
if (node.nodeType === DOM_TEXT_NODE) {
|
|
975
|
-
return node.textContent || ''
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
if (node.nodeType !== DOM_ELEMENT_NODE) {
|
|
979
|
-
return ''
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const tagName = node.tagName?.toLowerCase?.() || ''
|
|
983
|
-
|
|
984
|
-
if (tagName === 'br') {
|
|
985
|
-
return '\n'
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
const childText = Array.from(node.childNodes).map((childNode) => walk(childNode)).join('')
|
|
989
|
-
|
|
990
|
-
if (['p', 'div', 'h1', 'h2', 'h3', 'li'].includes(tagName)) {
|
|
991
|
-
return `${childText}\n`
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
if (tagName === 'tr') {
|
|
995
|
-
return `${childText}\n`
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
if (tagName === 'td' || tagName === 'th') {
|
|
999
|
-
return `${childText}\t`
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
return childText
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
return Array.from(container.childNodes)
|
|
1006
|
-
.map((childNode) => walk(childNode))
|
|
1007
|
-
.join('')
|
|
1008
|
-
.replace(/\t+$/gm, '')
|
|
1009
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
1010
|
-
.trim()
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
function hasVisibleHtmlContent(html = '') {
|
|
1014
|
-
if (!html) {
|
|
1015
|
-
return false
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const container = document.createElement('div')
|
|
1019
|
-
container.innerHTML = html
|
|
1020
|
-
|
|
1021
|
-
if (container.querySelector('table, img, svg, canvas')) {
|
|
1022
|
-
return true
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
return Boolean((container.textContent || '').replace(/\u00a0/g, ' ').trim())
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function hasEditableDisplayContent(key, fallbackText = '') {
|
|
1029
|
-
const richTextItem = getStoredRichTextItem(key)
|
|
1030
|
-
|
|
1031
|
-
if (richTextItem?.html !== undefined) {
|
|
1032
|
-
return hasVisibleHtmlContent(richTextItem.html)
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (richTextItem?.plainText !== undefined) {
|
|
1036
|
-
return Boolean(String(richTextItem.plainText || '').trim())
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
if (editableFieldTemplateHtmlMap[key]) {
|
|
1040
|
-
return hasVisibleHtmlContent(editableFieldTemplateHtmlMap[key])
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
return Boolean(String(fallbackText || '').trim())
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
function getFieldStoredHtml(key, fallbackText = '') {
|
|
1047
|
-
const richTextItem = getStoredRichTextItem(key)
|
|
1048
|
-
|
|
1049
|
-
if (richTextItem?.html) {
|
|
1050
|
-
return normalizeRichTextHtmlForStorage(richTextItem.html)
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (editableFieldTemplateHtmlMap[key]) {
|
|
1054
|
-
return normalizeRichTextHtmlForStorage(editableFieldTemplateHtmlMap[key])
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
return `<p>${escapeHtml(String(fallbackText || ''))}</p>`
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
function getFieldRenderableHtml(key, fallbackText = '') {
|
|
1061
|
-
const richTextItem = getStoredRichTextItem(key)
|
|
1062
|
-
|
|
1063
|
-
if (richTextItem?.html) {
|
|
1064
|
-
return normalizeRichTextHtmlForStorage(richTextItem.html)
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
if (editableFieldTemplateHtmlMap[key]) {
|
|
1068
|
-
return normalizeRichTextHtmlForStorage(editableFieldTemplateHtmlMap[key])
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
return `<p>${escapeHtml(String(fallbackText || ''))}</p>`
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function normalizeRichTextHtmlForStorage(html = '') {
|
|
1075
|
-
if (!html) {
|
|
1076
|
-
return ''
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
const container = document.createElement('div')
|
|
1080
|
-
container.innerHTML = html
|
|
1081
|
-
|
|
1082
|
-
container.querySelectorAll('.tableWrapper').forEach((wrapper) => {
|
|
1083
|
-
wrapper.style.width = '100%'
|
|
1084
|
-
wrapper.style.maxWidth = '100%'
|
|
1085
|
-
})
|
|
1086
|
-
|
|
1087
|
-
container.querySelectorAll('table').forEach((table) => {
|
|
1088
|
-
table.removeAttribute('width')
|
|
1089
|
-
table.style.width = '100%'
|
|
1090
|
-
table.style.minWidth = '100%'
|
|
1091
|
-
table.style.maxWidth = '100%'
|
|
1092
|
-
table.style.tableLayout = 'fixed'
|
|
1093
|
-
})
|
|
1094
|
-
|
|
1095
|
-
container.querySelectorAll('col').forEach((col) => {
|
|
1096
|
-
col.removeAttribute('width')
|
|
1097
|
-
col.style.width = ''
|
|
1098
|
-
})
|
|
1099
|
-
|
|
1100
|
-
container.querySelectorAll('td, th').forEach((cell) => {
|
|
1101
|
-
cell.style.textAlign = cell.style.textAlign || 'center'
|
|
1102
|
-
cell.style.verticalAlign = 'middle'
|
|
1103
|
-
})
|
|
1104
|
-
|
|
1105
|
-
return container.innerHTML
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
function buildRenderableSegments(html, fallbackText = '') {
|
|
1109
|
-
if (!html) {
|
|
1110
|
-
return [{ type: 'text', text: String(fallbackText || '') }]
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
const container = document.createElement('div')
|
|
1114
|
-
container.innerHTML = html
|
|
1115
|
-
const segments = []
|
|
1116
|
-
|
|
1117
|
-
function walk(node) {
|
|
1118
|
-
if (!node) {
|
|
1119
|
-
return
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
if (node.nodeType === DOM_TEXT_NODE) {
|
|
1123
|
-
const text = node.textContent || ''
|
|
1124
|
-
if (text) {
|
|
1125
|
-
segments.push({ type: 'text', text })
|
|
1126
|
-
}
|
|
1127
|
-
return
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
if (node.nodeType !== DOM_ELEMENT_NODE) {
|
|
1131
|
-
return
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
const tagName = node.tagName?.toLowerCase?.() || ''
|
|
1135
|
-
|
|
1136
|
-
if (tagName === 'br') {
|
|
1137
|
-
segments.push({ type: 'break' })
|
|
1138
|
-
return
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
Array.from(node.childNodes).forEach((childNode) => walk(childNode))
|
|
1142
|
-
|
|
1143
|
-
if (['p', 'div', 'h1', 'h2', 'h3', 'li'].includes(tagName)) {
|
|
1144
|
-
segments.push({ type: 'break' })
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
Array.from(container.childNodes).forEach((childNode) => walk(childNode))
|
|
1149
|
-
|
|
1150
|
-
while (segments.length && segments[segments.length - 1].type === 'break') {
|
|
1151
|
-
segments.pop()
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
return segments.length ? segments : [{ type: 'text', text: String(fallbackText || '') }]
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
function getClosestParagraphElement(node) {
|
|
1158
|
-
if (!node) {
|
|
1159
|
-
return null
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
const parentElement = node.nodeType === DOM_ELEMENT_NODE ? node : node.parentElement
|
|
1163
|
-
return parentElement?.closest?.('p') || null
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function normalizeEditableText(text = '') {
|
|
1167
|
-
return text.replace(/(\s|\u00a0|\u200b|\u200c|\u200d|\ufeff)/g, '')
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
function getRangeText(startNode, startOffset, endNode, endOffset) {
|
|
1171
|
-
const range = document.createRange()
|
|
1172
|
-
range.setStart(startNode, startOffset)
|
|
1173
|
-
range.setEnd(endNode, endOffset)
|
|
1174
|
-
return range.toString()
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
function getRangeFragment(startNode, startOffset, endNode, endOffset) {
|
|
1178
|
-
const range = document.createRange()
|
|
1179
|
-
range.setStart(startNode, startOffset)
|
|
1180
|
-
range.setEnd(endNode, endOffset)
|
|
1181
|
-
return range.cloneContents()
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
function isBlockBoundaryCharacter(char = '') {
|
|
1185
|
-
return !char || ['\n', '\r', '\f'].includes(char)
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
function isBlockByTextBoundary(combinedText = '', rangeStart = 0, rangeEnd = 0) {
|
|
1189
|
-
return isBlockBoundaryCharacter(combinedText[rangeStart - 1]) && isBlockBoundaryCharacter(combinedText[rangeEnd])
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function isDomBlockBoundary(node) {
|
|
1193
|
-
if (!node || node.nodeType !== DOM_ELEMENT_NODE) {
|
|
1194
|
-
return false
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
const tagName = node.tagName?.toLowerCase?.() || ''
|
|
1198
|
-
|
|
1199
|
-
if (tagName === 'br') {
|
|
1200
|
-
return true
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
const styleText = node.getAttribute('style') || ''
|
|
1204
|
-
return /(?:page-)?break-(?:before|after)\s*:\s*(?:page|always)/i.test(styleText)
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function getFragmentBoundaryText(fragment) {
|
|
1208
|
-
const parts = []
|
|
1209
|
-
|
|
1210
|
-
function walk(node) {
|
|
1211
|
-
if (!node) {
|
|
1212
|
-
return
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
if (node.nodeType === DOM_TEXT_NODE) {
|
|
1216
|
-
parts.push(node.textContent || '')
|
|
1217
|
-
return
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
if (node.nodeType !== DOM_ELEMENT_NODE) {
|
|
1221
|
-
return
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
if (isDomBlockBoundary(node)) {
|
|
1225
|
-
parts.push('\n')
|
|
1226
|
-
return
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
Array.from(node.childNodes).forEach((childNode) => walk(childNode))
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
Array.from(fragment.childNodes).forEach((childNode) => walk(childNode))
|
|
1233
|
-
return parts.join('')
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
function isCleanBoundaryFragment(fragment, direction) {
|
|
1237
|
-
const boundaryText = getFragmentBoundaryText(fragment)
|
|
1238
|
-
|
|
1239
|
-
if (!normalizeEditableText(boundaryText)) {
|
|
1240
|
-
return true
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
const textParts = boundaryText.split(/[\n\r\f]/)
|
|
1244
|
-
|
|
1245
|
-
if (direction === 'before') {
|
|
1246
|
-
return !normalizeEditableText(textParts[textParts.length - 1] || '')
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
return !normalizeEditableText(textParts[0] || '')
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function isStandaloneParagraphPlaceholder(startPosition, endPosition, fullText = '', combinedText = '', rangeStart = 0, rangeEnd = 0) {
|
|
1253
|
-
const { node: startNode, offset: startOffset } = startPosition
|
|
1254
|
-
const { node: endNode, offset: endOffset } = endPosition
|
|
1255
|
-
const startParagraph = getClosestParagraphElement(startNode)
|
|
1256
|
-
const endParagraph = getClosestParagraphElement(endNode)
|
|
1257
|
-
|
|
1258
|
-
if (!startParagraph || !endParagraph) {
|
|
1259
|
-
return isBlockByTextBoundary(combinedText, rangeStart, rangeEnd)
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
if (startParagraph !== endParagraph) {
|
|
1263
|
-
return true
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
const paragraphStartText = getRangeText(startParagraph, 0, startNode, startOffset)
|
|
1267
|
-
const paragraphEndOffset = startParagraph.childNodes.length
|
|
1268
|
-
const paragraphEndText = getRangeText(endNode, endOffset, startParagraph, paragraphEndOffset)
|
|
1269
|
-
const paragraphStartFragment = getRangeFragment(startParagraph, 0, startNode, startOffset)
|
|
1270
|
-
const paragraphEndFragment = getRangeFragment(endNode, endOffset, startParagraph, paragraphEndOffset)
|
|
1271
|
-
const markerText = normalizeEditableText(fullText)
|
|
1272
|
-
const paragraphText = normalizeEditableText(startParagraph.textContent || '')
|
|
1273
|
-
const hasOnlyWhitespaceAroundMarker =
|
|
1274
|
-
!normalizeEditableText(paragraphStartText) && !normalizeEditableText(paragraphEndText)
|
|
1275
|
-
const hasVisualBlockBoundary =
|
|
1276
|
-
isCleanBoundaryFragment(paragraphStartFragment, 'before') &&
|
|
1277
|
-
isCleanBoundaryFragment(paragraphEndFragment, 'after')
|
|
1278
|
-
|
|
1279
|
-
return hasOnlyWhitespaceAroundMarker || hasVisualBlockBoundary || paragraphText === markerText
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
function applySourceParagraphLayout(target, sourceParagraph) {
|
|
1283
|
-
if (!sourceParagraph) {
|
|
1284
|
-
return
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
const computedStyle = window.getComputedStyle(sourceParagraph)
|
|
1288
|
-
const inheritedProperties = [
|
|
1289
|
-
'textAlign',
|
|
1290
|
-
'marginTop',
|
|
1291
|
-
'marginBottom',
|
|
1292
|
-
'paddingTop',
|
|
1293
|
-
'paddingBottom',
|
|
1294
|
-
'textIndent',
|
|
1295
|
-
'lineHeight'
|
|
1296
|
-
]
|
|
1297
|
-
|
|
1298
|
-
inheritedProperties.forEach((property) => {
|
|
1299
|
-
target.style[property] = computedStyle[property]
|
|
1300
|
-
})
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
function escapeHtml(text) {
|
|
1304
|
-
return text
|
|
1305
|
-
.replace(/&/g, '&')
|
|
1306
|
-
.replace(/</g, '<')
|
|
1307
|
-
.replace(/>/g, '>')
|
|
1308
|
-
.replace(/"/g, '"')
|
|
1309
|
-
.replace(/'/g, ''')
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
function escapeXml(text = '') {
|
|
1313
|
-
return String(text)
|
|
1314
|
-
.replace(/&/g, '&')
|
|
1315
|
-
.replace(/</g, '<')
|
|
1316
|
-
.replace(/>/g, '>')
|
|
1317
|
-
.replace(/"/g, '"')
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function normalizeDocxColor(color = '') {
|
|
1321
|
-
const trimmedColor = String(color || '').trim()
|
|
1322
|
-
|
|
1323
|
-
if (/^#[0-9a-f]{6}$/i.test(trimmedColor)) {
|
|
1324
|
-
return trimmedColor.slice(1).toUpperCase()
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
const rgbMatch = trimmedColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i)
|
|
1328
|
-
|
|
1329
|
-
if (rgbMatch) {
|
|
1330
|
-
return [rgbMatch[1], rgbMatch[2], rgbMatch[3]]
|
|
1331
|
-
.map((value) => Number(value).toString(16).padStart(2, '0'))
|
|
1332
|
-
.join('')
|
|
1333
|
-
.toUpperCase()
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
return ''
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
function stripConflictingRunProperties(runProperties = '') {
|
|
1340
|
-
return runProperties
|
|
1341
|
-
.replace(/<w:b\b[^>]*\/>/g, '')
|
|
1342
|
-
.replace(/<w:b\b[\s\S]*?<\/w:b>/g, '')
|
|
1343
|
-
.replace(/<w:color\b[^>]*\/>/g, '')
|
|
1344
|
-
.replace(/<w:color\b[\s\S]*?<\/w:color>/g, '')
|
|
1345
|
-
.replace(/<w:highlight\b[^>]*\/>/g, '')
|
|
1346
|
-
.replace(/<w:highlight\b[\s\S]*?<\/w:highlight>/g, '')
|
|
1347
|
-
.replace(/<w:shd\b[^>]*\/>/g, '')
|
|
1348
|
-
.replace(/<w:shd\b[\s\S]*?<\/w:shd>/g, '')
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
function stripRunFontProperties(runProperties = '') {
|
|
1352
|
-
return runProperties
|
|
1353
|
-
.replace(/<w:rFonts\b[^>]*\/>/g, '')
|
|
1354
|
-
.replace(/<w:rFonts\b[\s\S]*?<\/w:rFonts>/g, '')
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
function stripRunSizeProperties(runProperties = '') {
|
|
1358
|
-
return runProperties
|
|
1359
|
-
.replace(/<w:sz\b[^>]*\/>/g, '')
|
|
1360
|
-
.replace(/<w:sz\b[\s\S]*?<\/w:sz>/g, '')
|
|
1361
|
-
.replace(/<w:szCs\b[^>]*\/>/g, '')
|
|
1362
|
-
.replace(/<w:szCs\b[\s\S]*?<\/w:szCs>/g, '')
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
function buildDocxRunFontProperties(fontName = DOCX_TABLE_FONT_NAME) {
|
|
1366
|
-
const escapedFontName = escapeXml(fontName)
|
|
1367
|
-
return `<w:rFonts w:hint="eastAsia" w:ascii="${escapedFontName}" w:hAnsi="${escapedFontName}" w:eastAsia="${escapedFontName}" w:cs="${escapedFontName}"/>`
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
function buildDocxRunSizeProperties(fontSizeHalfPoints = DOCX_TABLE_BODY_FONT_SIZE_HALF_POINTS) {
|
|
1371
|
-
return `<w:sz w:val="${fontSizeHalfPoints}"/><w:szCs w:val="${fontSizeHalfPoints}"/>`
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
function withDocxTableRunStyle(baseRunProperties = '', isHeader = false) {
|
|
1375
|
-
const cleanRunProperties = stripRunSizeProperties(stripRunFontProperties(baseRunProperties))
|
|
1376
|
-
const fontSize = isHeader
|
|
1377
|
-
? DOCX_TABLE_HEADER_FONT_SIZE_HALF_POINTS
|
|
1378
|
-
: DOCX_TABLE_BODY_FONT_SIZE_HALF_POINTS
|
|
1379
|
-
|
|
1380
|
-
return `${cleanRunProperties}${buildDocxRunFontProperties()}${buildDocxRunSizeProperties(fontSize)}`
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
function buildDocxRunProperties(baseRunProperties = '', marks = {}) {
|
|
1384
|
-
const properties = [stripConflictingRunProperties(baseRunProperties)]
|
|
1385
|
-
const textColor = normalizeDocxColor(marks.color)
|
|
1386
|
-
const backgroundColor = normalizeDocxColor(marks.backgroundColor)
|
|
1387
|
-
|
|
1388
|
-
if (marks.bold) {
|
|
1389
|
-
properties.push('<w:b/>')
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
if (textColor) {
|
|
1393
|
-
properties.push(`<w:color w:val="${textColor}"/>`)
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
if (backgroundColor) {
|
|
1397
|
-
properties.push(`<w:shd w:val="clear" w:color="auto" w:fill="${backgroundColor}"/>`)
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
const content = properties.join('')
|
|
1401
|
-
return content ? `<w:rPr>${content}</w:rPr>` : ''
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
function buildDocxTextRuns(text = '', marks = {}, baseRunProperties = '') {
|
|
1405
|
-
const parts = String(text).split('\n')
|
|
1406
|
-
const runProperties = buildDocxRunProperties(baseRunProperties, marks)
|
|
1407
|
-
const runs = []
|
|
1408
|
-
|
|
1409
|
-
parts.forEach((part, index) => {
|
|
1410
|
-
if (index > 0) {
|
|
1411
|
-
runs.push('<w:r><w:br/></w:r>')
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
if (part) {
|
|
1415
|
-
runs.push(`<w:r>${runProperties}<w:t xml:space="preserve">${escapeXml(part)}</w:t></w:r>`)
|
|
1416
|
-
}
|
|
1417
|
-
})
|
|
1418
|
-
|
|
1419
|
-
return runs.join('')
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
function buildDocxEmptyTextRun(baseRunProperties = '') {
|
|
1423
|
-
return `<w:r>${buildDocxRunProperties(baseRunProperties, {})}<w:t xml:space="preserve"></w:t></w:r>`
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function stripParagraphJustification(paragraphProperties = '') {
|
|
1427
|
-
return paragraphProperties
|
|
1428
|
-
.replace(/<w:jc\b[^>]*\/>/g, '')
|
|
1429
|
-
.replace(/<w:jc\b[\s\S]*?<\/w:jc>/g, '')
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
function stripParagraphIndent(paragraphProperties = '') {
|
|
1433
|
-
return paragraphProperties
|
|
1434
|
-
.replace(/<w:ind\b[^>]*\/>/g, '')
|
|
1435
|
-
.replace(/<w:ind\b[\s\S]*?<\/w:ind>/g, '')
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
function stripParagraphStyle(paragraphProperties = '') {
|
|
1439
|
-
return paragraphProperties
|
|
1440
|
-
.replace(/<w:pStyle\b[^>]*\/>/g, '')
|
|
1441
|
-
.replace(/<w:pStyle\b[\s\S]*?<\/w:pStyle>/g, '')
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
function normalizeDocxAlign(align = '') {
|
|
1445
|
-
const alignMap = {
|
|
1446
|
-
left: 'left',
|
|
1447
|
-
center: 'center',
|
|
1448
|
-
right: 'right'
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
return alignMap[align] || 'left'
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
function buildDocxParagraphProperties(baseParagraphProperties = '', align = 'left', textIndentLevel = 0, styleId = '') {
|
|
1455
|
-
const paragraphProperties = stripParagraphStyle(stripParagraphIndent(stripParagraphJustification(baseParagraphProperties)))
|
|
1456
|
-
const level = Math.max(0, Number(textIndentLevel || 0))
|
|
1457
|
-
const indentXml = level ? `<w:ind w:firstLine="${Math.round(level * 420)}"/>` : ''
|
|
1458
|
-
const styleXml = styleId ? `<w:pStyle w:val="${escapeXml(styleId)}"/>` : ''
|
|
1459
|
-
return `<w:pPr>${styleXml}${paragraphProperties}<w:jc w:val="${normalizeDocxAlign(align)}"/>${indentXml}</w:pPr>`
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
function getXmlText(xml = '') {
|
|
1463
|
-
return [...xml.matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)]
|
|
1464
|
-
.map((match) => decodeXmlText(match[1]))
|
|
1465
|
-
.join('')
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
function getParagraphProperties(paragraphXml = '') {
|
|
1469
|
-
return paragraphXml.match(/<w:pPr>([\s\S]*?)<\/w:pPr>/)?.[1] || ''
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
function getFirstRunProperties(paragraphXml = '') {
|
|
1473
|
-
return paragraphXml.match(/<w:rPr>([\s\S]*?)<\/w:rPr>/)?.[1] || ''
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
function buildDocxParagraph(runs = '', align = 'left', baseParagraphProperties = '', textIndentLevel = 0, styleId = '') {
|
|
1477
|
-
return `<w:p>${buildDocxParagraphProperties(baseParagraphProperties, align, textIndentLevel, styleId)}${runs || '<w:r><w:t xml:space="preserve"></w:t></w:r>'}</w:p>`
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
function getTableColumnCount(tableBlock = {}) {
|
|
1481
|
-
return Math.max(
|
|
1482
|
-
1,
|
|
1483
|
-
...(tableBlock.rows || []).map((row) => (row.cells || []).reduce((result, cell) => {
|
|
1484
|
-
return result + Number(cell.colSpan || 1)
|
|
1485
|
-
}, 0))
|
|
1486
|
-
)
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
function normalizeTableRowsForDocx(tableBlock = {}) {
|
|
1490
|
-
const columnCount = getTableColumnCount(tableBlock)
|
|
1491
|
-
const activeRowSpans = []
|
|
1492
|
-
|
|
1493
|
-
return (tableBlock.rows || []).map((row) => {
|
|
1494
|
-
const normalizedCells = []
|
|
1495
|
-
let columnIndex = 0
|
|
1496
|
-
|
|
1497
|
-
function appendVerticalContinuation() {
|
|
1498
|
-
const activeSpan = activeRowSpans[columnIndex]
|
|
1499
|
-
|
|
1500
|
-
if (!activeSpan?.remaining) {
|
|
1501
|
-
return false
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
normalizedCells.push({
|
|
1505
|
-
vMerge: 'continue',
|
|
1506
|
-
colSpan: activeSpan.colSpan || 1,
|
|
1507
|
-
blocks: []
|
|
1508
|
-
})
|
|
1509
|
-
activeSpan.remaining -= 1
|
|
1510
|
-
columnIndex += activeSpan.colSpan || 1
|
|
1511
|
-
return true
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
const rowCells = row.cells || []
|
|
1515
|
-
|
|
1516
|
-
rowCells.forEach((cell) => {
|
|
1517
|
-
while (appendVerticalContinuation()) {
|
|
1518
|
-
// Fill cells occupied by vertical merges from previous rows before placing the next real cell.
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
const colSpan = Number(cell.colSpan || 1)
|
|
1522
|
-
const rowSpan = Number(cell.rowSpan || 1)
|
|
1523
|
-
|
|
1524
|
-
normalizedCells.push({
|
|
1525
|
-
...cell,
|
|
1526
|
-
colSpan,
|
|
1527
|
-
rowSpan,
|
|
1528
|
-
vMerge: rowSpan > 1 ? 'restart' : ''
|
|
1529
|
-
})
|
|
1530
|
-
|
|
1531
|
-
if (rowSpan > 1) {
|
|
1532
|
-
activeRowSpans[columnIndex] = {
|
|
1533
|
-
remaining: rowSpan - 1,
|
|
1534
|
-
colSpan
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
columnIndex += colSpan
|
|
1539
|
-
})
|
|
1540
|
-
|
|
1541
|
-
while (columnIndex < columnCount) {
|
|
1542
|
-
if (!appendVerticalContinuation()) {
|
|
1543
|
-
normalizedCells.push({
|
|
1544
|
-
colSpan: 1,
|
|
1545
|
-
blocks: []
|
|
1546
|
-
})
|
|
1547
|
-
columnIndex += 1
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
return {
|
|
1552
|
-
cells: normalizedCells
|
|
1553
|
-
}
|
|
1554
|
-
})
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function buildDocxTableCell(cell = {}, columnWidthPct = 0, baseParagraphProperties = '', baseRunProperties = '') {
|
|
1558
|
-
const tableRunProperties = withDocxTableRunStyle(baseRunProperties, cell.header)
|
|
1559
|
-
const paragraphBlocks = (cell.blocks || []).filter((block) => block.type === 'paragraph' || block.type === 'heading')
|
|
1560
|
-
const cellXml = paragraphBlocks.length
|
|
1561
|
-
? paragraphBlocks.map((block) => {
|
|
1562
|
-
const runs = (block.runs || []).map((run) => {
|
|
1563
|
-
return buildDocxTextRuns(run.text || '', cell.header ? { ...(run.marks || {}), bold: true } : run.marks || {}, tableRunProperties)
|
|
1564
|
-
}).join('')
|
|
1565
|
-
|
|
1566
|
-
return buildDocxParagraph(runs, block.align || 'center', baseParagraphProperties)
|
|
1567
|
-
}).join('')
|
|
1568
|
-
: buildDocxParagraph(buildDocxEmptyTextRun(tableRunProperties), 'center', baseParagraphProperties)
|
|
1569
|
-
|
|
1570
|
-
const gridSpan = Number(cell.colSpan || 1) > 1
|
|
1571
|
-
? `<w:gridSpan w:val="${Number(cell.colSpan || 1)}"/>`
|
|
1572
|
-
: ''
|
|
1573
|
-
const shading = cell.header
|
|
1574
|
-
? '<w:shd w:val="clear" w:color="auto" w:fill="BFBFBF"/>'
|
|
1575
|
-
: ''
|
|
1576
|
-
const verticalMerge = cell.vMerge
|
|
1577
|
-
? `<w:vMerge${cell.vMerge === 'restart' ? ' w:val="restart"' : ''}/>`
|
|
1578
|
-
: ''
|
|
1579
|
-
|
|
1580
|
-
return `<w:tc><w:tcPr><w:tcW w:w="${columnWidthPct * Number(cell.colSpan || 1)}" w:type="pct"/>${gridSpan}${verticalMerge}${shading}<w:vAlign w:val="center"/><w:tcMar><w:top w:w="80" w:type="dxa"/><w:left w:w="80" w:type="dxa"/><w:bottom w:w="80" w:type="dxa"/><w:right w:w="80" w:type="dxa"/></w:tcMar></w:tcPr>${cellXml}</w:tc>`
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
function buildDocxTable(tableBlock = {}, baseParagraphProperties = '', baseRunProperties = '') {
|
|
1584
|
-
const columnCount = getTableColumnCount(tableBlock)
|
|
1585
|
-
const columnWidth = Math.max(1, Math.floor(DOCX_TABLE_WIDTH_DXA / columnCount))
|
|
1586
|
-
const columnWidthPct = Math.max(1, Math.floor(DOCX_TABLE_WIDTH_PCT / columnCount))
|
|
1587
|
-
const tableGrid = Array.from({ length: columnCount }, () => `<w:gridCol w:w="${columnWidth}"/>`).join('')
|
|
1588
|
-
const rowsXml = normalizeTableRowsForDocx(tableBlock).map((row) => {
|
|
1589
|
-
const cellsXml = (row.cells || []).map((cell) => {
|
|
1590
|
-
return buildDocxTableCell(cell, columnWidthPct, baseParagraphProperties, baseRunProperties)
|
|
1591
|
-
}).join('')
|
|
1592
|
-
|
|
1593
|
-
return `<w:tr>${cellsXml}</w:tr>`
|
|
1594
|
-
}).join('')
|
|
1595
|
-
|
|
1596
|
-
return `<w:tbl><w:tblPr><w:tblW w:w="${DOCX_TABLE_WIDTH_PCT}" w:type="pct"/><w:jc w:val="center"/><w:tblLayout w:type="fixed"/><w:tblBorders><w:top w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:left w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:bottom w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:right w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:insideH w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:insideV w:val="single" w:sz="8" w:space="0" w:color="000000"/></w:tblBorders></w:tblPr><w:tblGrid>${tableGrid}</w:tblGrid>${rowsXml}</w:tbl>`
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
function createInlineMarks(marks = []) {
|
|
1600
|
-
return marks.reduce((result, mark) => {
|
|
1601
|
-
if (mark.type === 'bold') {
|
|
1602
|
-
result.bold = true
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
if (mark.type === 'textStyle' && mark.attrs?.color) {
|
|
1606
|
-
result.color = mark.attrs.color
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
if (mark.type === 'highlight' && mark.attrs?.color) {
|
|
1610
|
-
result.backgroundColor = mark.attrs.color
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
return result
|
|
1614
|
-
}, {})
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
function getHtmlNodeTextAlign(node) {
|
|
1618
|
-
return getHtmlNodeTextAlignWithDefault(node)
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
function getHtmlNodeTextIndentLevel(node) {
|
|
1622
|
-
if (!node || node.nodeType !== DOM_ELEMENT_NODE) {
|
|
1623
|
-
return 0
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
const indent = node.style?.textIndent || ''
|
|
1627
|
-
const match = indent.match(/^([\d.]+)em$/)
|
|
1628
|
-
|
|
1629
|
-
if (!match) {
|
|
1630
|
-
return 0
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
return Math.min(
|
|
1634
|
-
EDITOR_MAX_INDENT_LEVEL,
|
|
1635
|
-
Math.max(0, Math.round(Number(match[1]) / EDITOR_INDENT_STEP_EM))
|
|
1636
|
-
)
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
function getHtmlNodeTextAlignWithDefault(node, defaultAlign = 'left') {
|
|
1640
|
-
if (!node || node.nodeType !== DOM_ELEMENT_NODE) {
|
|
1641
|
-
return defaultAlign
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
const inlineAlign = node.style?.textAlign || ''
|
|
1645
|
-
|
|
1646
|
-
if (['left', 'center', 'right'].includes(inlineAlign)) {
|
|
1647
|
-
return inlineAlign
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
const alignAttr = node.getAttribute?.('align') || ''
|
|
1651
|
-
|
|
1652
|
-
if (['left', 'center', 'right'].includes(alignAttr)) {
|
|
1653
|
-
return alignAttr
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
return defaultAlign
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
function getHtmlNodeMarks(node, inheritedMarks = {}) {
|
|
1660
|
-
if (!node || node.nodeType !== DOM_ELEMENT_NODE) {
|
|
1661
|
-
return inheritedMarks
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
const tagName = node.tagName?.toLowerCase?.() || ''
|
|
1665
|
-
const nextMarks = { ...inheritedMarks }
|
|
1666
|
-
|
|
1667
|
-
if (tagName === 'strong' || tagName === 'b') {
|
|
1668
|
-
nextMarks.bold = true
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
const textColor = node.style?.color ? normalizeDocxColor(node.style.color) : ''
|
|
1672
|
-
const backgroundColor = node.style?.backgroundColor ? normalizeDocxColor(node.style.backgroundColor) : ''
|
|
1673
|
-
|
|
1674
|
-
if (textColor) {
|
|
1675
|
-
nextMarks.color = `#${textColor}`
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
if (backgroundColor) {
|
|
1679
|
-
nextMarks.backgroundColor = `#${backgroundColor}`
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
return nextMarks
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
function extractRunsFromHtmlNode(node, inheritedMarks = {}) {
|
|
1686
|
-
if (!node) {
|
|
1687
|
-
return []
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
if (node.nodeType === DOM_TEXT_NODE) {
|
|
1691
|
-
return node.textContent
|
|
1692
|
-
? [{
|
|
1693
|
-
text: node.textContent,
|
|
1694
|
-
marks: inheritedMarks
|
|
1695
|
-
}]
|
|
1696
|
-
: []
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
if (node.nodeType !== DOM_ELEMENT_NODE) {
|
|
1700
|
-
return []
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
const tagName = node.tagName?.toLowerCase?.() || ''
|
|
1704
|
-
const nextMarks = getHtmlNodeMarks(node, inheritedMarks)
|
|
1705
|
-
|
|
1706
|
-
if (tagName === 'br') {
|
|
1707
|
-
return [{
|
|
1708
|
-
text: '\n',
|
|
1709
|
-
marks: nextMarks
|
|
1710
|
-
}]
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
return Array.from(node.childNodes).flatMap((childNode) => extractRunsFromHtmlNode(childNode, nextMarks))
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
function normalizeRuns(runs = []) {
|
|
1717
|
-
return runs.reduce((result, run) => {
|
|
1718
|
-
if (!run?.text) {
|
|
1719
|
-
return result
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
const previousRun = result[result.length - 1]
|
|
1723
|
-
const marksKey = JSON.stringify(run.marks || {})
|
|
1724
|
-
const previousMarksKey = JSON.stringify(previousRun?.marks || {})
|
|
1725
|
-
|
|
1726
|
-
if (previousRun && marksKey === previousMarksKey) {
|
|
1727
|
-
previousRun.text += run.text
|
|
1728
|
-
return result
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
result.push({
|
|
1732
|
-
text: run.text,
|
|
1733
|
-
marks: run.marks || {}
|
|
1734
|
-
})
|
|
1735
|
-
return result
|
|
1736
|
-
}, [])
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
function extractBlocksFromHtml(html = '') {
|
|
1740
|
-
if (!html) {
|
|
1741
|
-
return []
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
const container = document.createElement('div')
|
|
1745
|
-
container.innerHTML = html
|
|
1746
|
-
|
|
1747
|
-
function extractBlockFromElement(element) {
|
|
1748
|
-
const tagName = element.tagName?.toLowerCase?.() || ''
|
|
1749
|
-
|
|
1750
|
-
if (tagName === 'table') {
|
|
1751
|
-
return {
|
|
1752
|
-
type: 'table',
|
|
1753
|
-
rows: Array.from(element.rows || []).map((row) => ({
|
|
1754
|
-
cells: Array.from(row.cells || []).map((cell) => {
|
|
1755
|
-
const childBlocks = Array.from(cell.childNodes).flatMap((childNode) => {
|
|
1756
|
-
if (childNode.nodeType === DOM_TEXT_NODE) {
|
|
1757
|
-
const text = childNode.textContent?.trim?.() || ''
|
|
1758
|
-
return text
|
|
1759
|
-
? [{
|
|
1760
|
-
type: 'paragraph',
|
|
1761
|
-
align: getHtmlNodeTextAlignWithDefault(cell, 'center'),
|
|
1762
|
-
textIndentLevel: 0,
|
|
1763
|
-
runs: [{ text, marks: {} }]
|
|
1764
|
-
}]
|
|
1765
|
-
: []
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
if (childNode.nodeType !== DOM_ELEMENT_NODE) {
|
|
1769
|
-
return []
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
const childTagName = childNode.tagName?.toLowerCase?.() || ''
|
|
1773
|
-
|
|
1774
|
-
if (['p', 'div', 'h1', 'h2', 'h3'].includes(childTagName)) {
|
|
1775
|
-
const childBlock = extractBlockFromElement(childNode)
|
|
1776
|
-
|
|
1777
|
-
if (childBlock && !childNode.style?.textAlign && !childNode.getAttribute?.('align')) {
|
|
1778
|
-
childBlock.align = getHtmlNodeTextAlignWithDefault(cell, 'center')
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
return childBlock ? [childBlock] : []
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
return []
|
|
1785
|
-
})
|
|
1786
|
-
|
|
1787
|
-
return {
|
|
1788
|
-
header: cell.tagName?.toLowerCase?.() === 'th',
|
|
1789
|
-
colSpan: Number(cell.colSpan || 1),
|
|
1790
|
-
rowSpan: Number(cell.rowSpan || 1),
|
|
1791
|
-
blocks: childBlocks.length
|
|
1792
|
-
? childBlocks
|
|
1793
|
-
: [{
|
|
1794
|
-
type: 'paragraph',
|
|
1795
|
-
align: getHtmlNodeTextAlignWithDefault(cell, 'center'),
|
|
1796
|
-
textIndentLevel: 0,
|
|
1797
|
-
runs: []
|
|
1798
|
-
}]
|
|
1799
|
-
}
|
|
1800
|
-
})
|
|
1801
|
-
}))
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
if (!['p', 'div', 'h1', 'h2', 'h3'].includes(tagName)) {
|
|
1806
|
-
return null
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
return {
|
|
1810
|
-
type: tagName.startsWith('h') ? 'heading' : 'paragraph',
|
|
1811
|
-
level: tagName.startsWith('h') ? Number(tagName.slice(1)) : null,
|
|
1812
|
-
align: getHtmlNodeTextAlign(element),
|
|
1813
|
-
textIndentLevel: getHtmlNodeTextIndentLevel(element),
|
|
1814
|
-
runs: normalizeRuns(extractRunsFromHtmlNode(element))
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
const blocks = Array.from(container.childNodes).flatMap((node) => {
|
|
1819
|
-
if (node.nodeType === DOM_TEXT_NODE) {
|
|
1820
|
-
const text = node.textContent?.trim?.() || ''
|
|
1821
|
-
return text
|
|
1822
|
-
? [{
|
|
1823
|
-
type: 'paragraph',
|
|
1824
|
-
align: 'left',
|
|
1825
|
-
textIndentLevel: 0,
|
|
1826
|
-
runs: [{ text, marks: {} }]
|
|
1827
|
-
}]
|
|
1828
|
-
: []
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
if (node.nodeType !== DOM_ELEMENT_NODE) {
|
|
1832
|
-
return []
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
const tagName = node.tagName?.toLowerCase?.() || ''
|
|
1836
|
-
|
|
1837
|
-
if (!['p', 'div', 'h1', 'h2', 'h3', 'table'].includes(tagName)) {
|
|
1838
|
-
return []
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
const block = extractBlockFromElement(node)
|
|
1842
|
-
return block ? [block] : []
|
|
1843
|
-
})
|
|
1844
|
-
|
|
1845
|
-
return blocks.filter((block) => {
|
|
1846
|
-
if (block.type === 'table') {
|
|
1847
|
-
return block.rows?.length
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
return block.runs?.some((run) => run.text)
|
|
1851
|
-
})
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
function extractTextRuns(contentList = []) {
|
|
1855
|
-
return contentList.reduce((result, item) => {
|
|
1856
|
-
if (!item) {
|
|
1857
|
-
return result
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
if (item.type === 'text') {
|
|
1861
|
-
result.push({
|
|
1862
|
-
text: item.text || '',
|
|
1863
|
-
marks: createInlineMarks(item.marks)
|
|
1864
|
-
})
|
|
1865
|
-
return result
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
if (Array.isArray(item.content)) {
|
|
1869
|
-
result.push(...extractTextRuns(item.content))
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
return result
|
|
1873
|
-
}, [])
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
function convertRichTextNodeToBlock(node) {
|
|
1877
|
-
if (!node) {
|
|
1878
|
-
return null
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
if (node.type === 'paragraph' || node.type === 'heading') {
|
|
1882
|
-
return {
|
|
1883
|
-
type: node.type,
|
|
1884
|
-
level: node.attrs?.level || null,
|
|
1885
|
-
align: node.attrs?.textAlign || 'left',
|
|
1886
|
-
textIndentLevel: Number(node.attrs?.textIndentLevel || 0),
|
|
1887
|
-
runs: extractTextRuns(node.content || [])
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
return {
|
|
1892
|
-
type: node.type,
|
|
1893
|
-
runs: extractTextRuns(node.content || [])
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
function buildBackendRichTextValue(fieldKey, path, html, json, plainText) {
|
|
1898
|
-
const normalizedHtml = normalizeRichTextHtmlForStorage(html)
|
|
1899
|
-
const htmlBlocks = extractBlocksFromHtml(normalizedHtml)
|
|
1900
|
-
const jsonBlocks = (json?.content || [])
|
|
1901
|
-
.map((node) => convertRichTextNodeToBlock(node))
|
|
1902
|
-
.filter(Boolean)
|
|
1903
|
-
|
|
1904
|
-
return {
|
|
1905
|
-
version: '1.0',
|
|
1906
|
-
fieldKey,
|
|
1907
|
-
path,
|
|
1908
|
-
docxRenderText: plainText,
|
|
1909
|
-
plainText,
|
|
1910
|
-
html: normalizedHtml,
|
|
1911
|
-
json,
|
|
1912
|
-
blocks: htmlBlocks.length ? htmlBlocks : jsonBlocks,
|
|
1913
|
-
updatedAt: new Date().toISOString()
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
function getRichTextBlocks(richItem = {}) {
|
|
1918
|
-
if (!richItem) {
|
|
1919
|
-
return []
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
if (Array.isArray(richItem.blocks) && richItem.blocks.length) {
|
|
1923
|
-
return richItem.blocks
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
return (richItem.json?.content || [])
|
|
1927
|
-
.map((node) => convertRichTextNodeToBlock(node))
|
|
1928
|
-
.filter(Boolean)
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
function buildDocxRichTextRuns(richItem = {}, baseRunProperties = '') {
|
|
1932
|
-
const normalizedRichItem = richItem || {}
|
|
1933
|
-
const blocks = getRichTextBlocks(richItem).filter((block) => block.type !== 'table')
|
|
1934
|
-
|
|
1935
|
-
if (!blocks.length && normalizedRichItem.plainText) {
|
|
1936
|
-
return buildDocxTextRuns(normalizedRichItem.plainText, {}, baseRunProperties)
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
return blocks.map((block, blockIndex) => {
|
|
1940
|
-
const blockRuns = (block.runs || []).map((run) => {
|
|
1941
|
-
return buildDocxTextRuns(run.text || '', run.marks || {}, baseRunProperties)
|
|
1942
|
-
}).join('')
|
|
1943
|
-
|
|
1944
|
-
return `${blockIndex > 0 ? '<w:r><w:br/></w:r>' : ''}${blockRuns}`
|
|
1945
|
-
}).join('')
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
function formatHeadingNumber(level, counters) {
|
|
1949
|
-
if (level === 1) {
|
|
1950
|
-
const chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
|
1951
|
-
return `${chineseNumbers[counters[0] - 1] || counters[0]}、`
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
if (level === 2) {
|
|
1955
|
-
const chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
|
1956
|
-
return `(${chineseNumbers[counters[1] - 1] || counters[1]})`
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
return `(${counters[2]})`
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
function stripExistingHeadingNumber(text = '') {
|
|
1963
|
-
return String(text || '')
|
|
1964
|
-
.replace(/^([一二三四五六七八九十]+、|([一二三四五六七八九十\d]+)|\(\d+\)|\d+[.、])\s*/, '')
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
function applyAutoHeadingNumber(block, counters) {
|
|
1968
|
-
if (block.type !== 'heading' || !block.level || block.__numberApplied) {
|
|
1969
|
-
return block
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
const level = Math.min(3, Math.max(1, Number(block.level || 1)))
|
|
1973
|
-
counters[level - 1] += 1
|
|
1974
|
-
|
|
1975
|
-
for (let index = level; index < counters.length; index += 1) {
|
|
1976
|
-
counters[index] = 0
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
const runs = block.runs || []
|
|
1980
|
-
const firstRun = runs.find((run) => run.text)
|
|
1981
|
-
const prefix = formatHeadingNumber(level, counters)
|
|
1982
|
-
|
|
1983
|
-
if (firstRun) {
|
|
1984
|
-
firstRun.text = `${prefix}${stripExistingHeadingNumber(firstRun.text)}`
|
|
1985
|
-
} else {
|
|
1986
|
-
runs.unshift({ text: prefix, marks: {} })
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
block.runs = runs
|
|
1990
|
-
block.__numberApplied = true
|
|
1991
|
-
return block
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
function getBlockParagraphStyleId(block, renderContext = {}) {
|
|
1995
|
-
if (block.type === 'heading') {
|
|
1996
|
-
return renderContext.headingStyleIds?.[block.level] || ''
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
return renderContext.bodyStyleId || ''
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
function buildDocxRichTextParagraphs(richItem = {}, baseParagraphProperties = '', baseRunProperties = '', beforeText = '', afterText = '', renderContext = {}) {
|
|
2003
|
-
const normalizedRichItem = richItem || {}
|
|
2004
|
-
const blocks = getRichTextBlocks(richItem)
|
|
2005
|
-
const renderBlocks = blocks.length
|
|
2006
|
-
? blocks
|
|
2007
|
-
: [{
|
|
2008
|
-
type: 'paragraph',
|
|
2009
|
-
align: 'left',
|
|
2010
|
-
textIndentLevel: 0,
|
|
2011
|
-
runs: [{ text: normalizedRichItem.plainText || '', marks: {} }]
|
|
2012
|
-
}]
|
|
2013
|
-
|
|
2014
|
-
const normalizedBlocks = deepClone(renderBlocks)
|
|
2015
|
-
const headingCounters = [...(renderContext.headingCounters || [0, 0, 0])]
|
|
2016
|
-
|
|
2017
|
-
if (beforeText) {
|
|
2018
|
-
if (normalizedBlocks[0]?.type === 'paragraph' || normalizedBlocks[0]?.type === 'heading') {
|
|
2019
|
-
normalizedBlocks[0].runs = [{ text: beforeText, marks: {} }, ...(normalizedBlocks[0].runs || [])]
|
|
2020
|
-
} else {
|
|
2021
|
-
normalizedBlocks.unshift({
|
|
2022
|
-
type: 'paragraph',
|
|
2023
|
-
align: 'left',
|
|
2024
|
-
textIndentLevel: 0,
|
|
2025
|
-
runs: [{ text: beforeText, marks: {} }]
|
|
2026
|
-
})
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
if (afterText) {
|
|
2031
|
-
const lastBlock = normalizedBlocks[normalizedBlocks.length - 1]
|
|
2032
|
-
|
|
2033
|
-
if (lastBlock?.type === 'paragraph' || lastBlock?.type === 'heading') {
|
|
2034
|
-
lastBlock.runs = [...(lastBlock.runs || []), { text: afterText, marks: {} }]
|
|
2035
|
-
} else {
|
|
2036
|
-
normalizedBlocks.push({
|
|
2037
|
-
type: 'paragraph',
|
|
2038
|
-
align: 'left',
|
|
2039
|
-
textIndentLevel: 0,
|
|
2040
|
-
runs: [{ text: afterText, marks: {} }]
|
|
2041
|
-
})
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
return normalizedBlocks.map((sourceBlock) => {
|
|
2046
|
-
const block = applyAutoHeadingNumber(sourceBlock, headingCounters)
|
|
2047
|
-
|
|
2048
|
-
if (block.type === 'table') {
|
|
2049
|
-
return buildDocxTable(block, baseParagraphProperties, baseRunProperties)
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
const runs = (block.runs || []).map((run) => {
|
|
2053
|
-
return buildDocxTextRuns(run.text || '', run.marks || {}, baseRunProperties)
|
|
2054
|
-
}).join('')
|
|
2055
|
-
|
|
2056
|
-
return buildDocxParagraph(
|
|
2057
|
-
runs,
|
|
2058
|
-
block.align || 'left',
|
|
2059
|
-
baseParagraphProperties,
|
|
2060
|
-
block.textIndentLevel || 0,
|
|
2061
|
-
getBlockParagraphStyleId(block, renderContext)
|
|
2062
|
-
)
|
|
2063
|
-
}).join('')
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
function hasRichTextTableBlock(richItem = {}) {
|
|
2067
|
-
return getRichTextBlocks(richItem).some((block) => block.type === 'table')
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
function getRichTextFirstTableHtml(richItem = {}) {
|
|
2071
|
-
if (!richItem?.html) {
|
|
2072
|
-
return ''
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
const container = document.createElement('div')
|
|
2076
|
-
container.innerHTML = richItem.html
|
|
2077
|
-
const table = container.querySelector('table')
|
|
2078
|
-
|
|
2079
|
-
return table?.outerHTML || ''
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
function replaceTokenParagraphAndFollowingTable(xmlText = '', token = '', richItem = {}, renderContext = {}) {
|
|
2083
|
-
const escapedToken = escapeRegExp(token)
|
|
2084
|
-
const tokenParagraphAndTableRegExp = new RegExp(
|
|
2085
|
-
`<w:p\\b(?:(?!<w:p\\b)[\\s\\S])*?${escapedToken}(?:(?!<w:p\\b)[\\s\\S])*?<\\/w:p>\\s*<w:tbl\\b[\\s\\S]*?<\\/w:tbl>`,
|
|
2086
|
-
'g'
|
|
2087
|
-
)
|
|
2088
|
-
|
|
2089
|
-
return xmlText.replace(tokenParagraphAndTableRegExp, (matchedXml) => {
|
|
2090
|
-
const baseParagraphProperties = getParagraphProperties(matchedXml)
|
|
2091
|
-
const baseRunProperties = getFirstRunProperties(matchedXml)
|
|
2092
|
-
return buildDocxRichTextParagraphs(
|
|
2093
|
-
richItem,
|
|
2094
|
-
baseParagraphProperties,
|
|
2095
|
-
baseRunProperties,
|
|
2096
|
-
'',
|
|
2097
|
-
'',
|
|
2098
|
-
renderContext
|
|
2099
|
-
)
|
|
2100
|
-
})
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
function getRichTextRenderContext(fieldKey, options = {}) {
|
|
2104
|
-
const context = options.wordStyleContext || {}
|
|
2105
|
-
|
|
2106
|
-
return {
|
|
2107
|
-
headingStyleIds: context.headingStyleIds || {},
|
|
2108
|
-
bodyStyleId: context.bodyStyleId || '',
|
|
2109
|
-
headingCounters: context.editableHeadingCounters?.[fieldKey] || [0, 0, 0]
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
function replaceRichTextTokensInXml(xmlText = '', richTextMap = {}, options = {}) {
|
|
2114
|
-
return Object.keys(richTextMap).reduce((result, fieldKey) => {
|
|
2115
|
-
const richItem = richTextMap[fieldKey]
|
|
2116
|
-
const renderContext = getRichTextRenderContext(fieldKey, options)
|
|
2117
|
-
|
|
2118
|
-
if (!hasRichTextContent(richItem)) {
|
|
2119
|
-
return result
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
const token = getRichTextToken(fieldKey)
|
|
2123
|
-
|
|
2124
|
-
if (!result.includes(token)) {
|
|
2125
|
-
return result
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
if (hasRichTextTableBlock(richItem)) {
|
|
2129
|
-
result = replaceTokenParagraphAndFollowingTable(result, token, richItem, renderContext)
|
|
2130
|
-
|
|
2131
|
-
if (!result.includes(token)) {
|
|
2132
|
-
return result
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
result = result.replace(
|
|
2136
|
-
/<w:tbl\b[\s\S]*?<\/w:tbl>/g,
|
|
2137
|
-
(tableXml) => {
|
|
2138
|
-
if (!tableXml.includes(token)) {
|
|
2139
|
-
return tableXml
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
return buildDocxRichTextParagraphs(
|
|
2143
|
-
richItem,
|
|
2144
|
-
getParagraphProperties(tableXml),
|
|
2145
|
-
getFirstRunProperties(tableXml),
|
|
2146
|
-
'',
|
|
2147
|
-
'',
|
|
2148
|
-
renderContext
|
|
2149
|
-
)
|
|
2150
|
-
}
|
|
2151
|
-
)
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
if (!result.includes(token)) {
|
|
2155
|
-
return result
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
const paragraphPatchedXml = result.replace(
|
|
2159
|
-
/<w:p\b[^>]*>[\s\S]*?<\/w:p>/g,
|
|
2160
|
-
(paragraphXml) => {
|
|
2161
|
-
if (!paragraphXml.includes(token)) {
|
|
2162
|
-
return paragraphXml
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
const paragraphText = getXmlText(paragraphXml)
|
|
2166
|
-
|
|
2167
|
-
if (!paragraphText.includes(token)) {
|
|
2168
|
-
return paragraphXml
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
const [beforeText, afterText] = paragraphText.split(token)
|
|
2172
|
-
const blocks = getRichTextBlocks(richItem)
|
|
2173
|
-
const isStandaloneToken = !normalizeEditableText(beforeText) && !normalizeEditableText(afterText)
|
|
2174
|
-
const hasExplicitParagraphAlign = blocks.some((block) => ['center', 'right'].includes(block.align))
|
|
2175
|
-
|
|
2176
|
-
if (!isStandaloneToken && !hasExplicitParagraphAlign && blocks.length <= 1) {
|
|
2177
|
-
return paragraphXml
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
return buildDocxRichTextParagraphs(
|
|
2181
|
-
richItem,
|
|
2182
|
-
getParagraphProperties(paragraphXml),
|
|
2183
|
-
getFirstRunProperties(paragraphXml),
|
|
2184
|
-
beforeText,
|
|
2185
|
-
afterText,
|
|
2186
|
-
renderContext
|
|
2187
|
-
)
|
|
2188
|
-
}
|
|
2189
|
-
)
|
|
2190
|
-
|
|
2191
|
-
if (!paragraphPatchedXml.includes(token)) {
|
|
2192
|
-
return paragraphPatchedXml
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
return paragraphPatchedXml.replace(
|
|
2196
|
-
/<w:r\b[\s\S]*?<\/w:r>/g,
|
|
2197
|
-
(runXml) => {
|
|
2198
|
-
if (!runXml.includes(token)) {
|
|
2199
|
-
return runXml
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
const runProperties = runXml.match(/<w:rPr>([\s\S]*?)<\/w:rPr>/)?.[1] || ''
|
|
2203
|
-
const textMatches = [...runXml.matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)]
|
|
2204
|
-
const runText = textMatches.map((match) => decodeXmlText(match[1])).join('')
|
|
2205
|
-
|
|
2206
|
-
if (!runText.includes(token)) {
|
|
2207
|
-
return runXml
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
const [beforeText, afterText] = runText.split(token)
|
|
2211
|
-
const beforeRuns = buildDocxTextRuns(beforeText, {}, runProperties)
|
|
2212
|
-
const richRuns = buildDocxRichTextRuns(richItem, runProperties)
|
|
2213
|
-
const afterRuns = buildDocxTextRuns(afterText, {}, runProperties)
|
|
2214
|
-
|
|
2215
|
-
return `${beforeRuns}${richRuns}${afterRuns}`
|
|
2216
|
-
}
|
|
2217
|
-
)
|
|
2218
|
-
}, xmlText)
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
function applyRichTextTokensToDocx(zip, richTextMap = {}, options = {}) {
|
|
2222
|
-
const documentXmlFile = zip.file('word/document.xml')
|
|
2223
|
-
|
|
2224
|
-
if (!documentXmlFile) {
|
|
2225
|
-
return
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
const documentXml = documentXmlFile.asText()
|
|
2229
|
-
const patchedXml = replaceRichTextTokensInXml(documentXml, richTextMap, options)
|
|
2230
|
-
|
|
2231
|
-
if (patchedXml !== documentXml) {
|
|
2232
|
-
zip.file('word/document.xml', patchedXml)
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
function buildSavePayload() {
|
|
2237
|
-
const richTextMap = editableRenderData.value.__editableRichTextMap || {}
|
|
2238
|
-
|
|
2239
|
-
return {
|
|
2240
|
-
renderData: deepClone(editableRenderData.value),
|
|
2241
|
-
richTextFields: deepClone(richTextMap),
|
|
2242
|
-
editableFields: editableFieldKeys.value.map((fieldKey) => ({
|
|
2243
|
-
fieldKey,
|
|
2244
|
-
path: editableFieldPathMap.value[fieldKey] || '',
|
|
2245
|
-
hasRichText: Boolean(richTextMap[fieldKey])
|
|
2246
|
-
}))
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
function getRenderableFieldHtml(key, fallbackText = '') {
|
|
2251
|
-
const richTextMap = editableRenderData.value.__editableRichTextMap || {}
|
|
2252
|
-
const richTextItem = richTextMap[key]
|
|
2253
|
-
|
|
2254
|
-
return buildRenderableSegments(richTextItem?.html, fallbackText)
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
function cleanupEditorMarkers() {
|
|
2258
|
-
if (!previewRef.value) {
|
|
2259
|
-
return
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
const textNodes = getAllTextNodes(previewRef.value)
|
|
2263
|
-
|
|
2264
|
-
textNodes.forEach((node) => {
|
|
2265
|
-
const text = node.textContent || ''
|
|
2266
|
-
|
|
2267
|
-
if (!text) {
|
|
2268
|
-
return
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
const cleanedText = text
|
|
2272
|
-
.replaceAll(`${LEGACY_EDITOR_MARKER_START}${EDITOR_MARKER_START}`, '')
|
|
2273
|
-
.replaceAll(`${EDITOR_MARKER_END}${LEGACY_EDITOR_MARKER_END}`, '')
|
|
2274
|
-
.replaceAll(`${LEGACY_EDITOR_MARKER_START} ${EDITOR_MARKER_START}`, '')
|
|
2275
|
-
.replaceAll(`${EDITOR_MARKER_END} ${LEGACY_EDITOR_MARKER_END}`, '')
|
|
2276
|
-
.replaceAll(EDITOR_MARKER_START, '')
|
|
2277
|
-
.replaceAll(EDITOR_MARKER_END, '')
|
|
2278
|
-
.replaceAll(LEGACY_EDITOR_MARKER_START, '')
|
|
2279
|
-
.replaceAll(LEGACY_EDITOR_MARKER_END, '')
|
|
2280
|
-
|
|
2281
|
-
if (cleanedText !== text) {
|
|
2282
|
-
node.textContent = cleanedText
|
|
2283
|
-
}
|
|
2284
|
-
})
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
function normalizeWaterLevelChartConfig(config = {}) {
|
|
2288
|
-
const xAxisData = config.xAxisData || config.xAxis || config.categories || []
|
|
2289
|
-
const yAxisData = config.yAxisData || config.yAxis || config.values || []
|
|
2290
|
-
const upperLimit = Number(
|
|
2291
|
-
config.upperLimit ?? config.reasonableUpperLimit ?? config.maxReasonableValue ?? 80
|
|
2292
|
-
)
|
|
2293
|
-
const lowerLimit = Number(
|
|
2294
|
-
config.lowerLimit ?? config.reasonableLowerLimit ?? config.minReasonableValue ?? 30
|
|
2295
|
-
)
|
|
2296
|
-
const regionList = config.regionList || config.regions || config.areas || []
|
|
2297
|
-
|
|
2298
|
-
return {
|
|
2299
|
-
title: config.title || '',
|
|
2300
|
-
xAxisData,
|
|
2301
|
-
yAxisData,
|
|
2302
|
-
upperLimit,
|
|
2303
|
-
lowerLimit,
|
|
2304
|
-
regionList
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
function buildWaterLevelChartOption(config = {}, tagName = '') {
|
|
2309
|
-
const chartConfig = normalizeWaterLevelChartConfig(config, tagName)
|
|
2310
|
-
const total = chartConfig.xAxisData.length || 1
|
|
2311
|
-
const plotWidth = WATER_LEVEL_CHART_CANVAS_WIDTH - WATER_LEVEL_CHART_GRID.left - WATER_LEVEL_CHART_GRID.right
|
|
2312
|
-
const plotBottom = WATER_LEVEL_CHART_CANVAS_HEIGHT - WATER_LEVEL_CHART_GRID.bottom
|
|
2313
|
-
const regionGraphics = []
|
|
2314
|
-
let consumedCount = 0
|
|
2315
|
-
|
|
2316
|
-
chartConfig.regionList.forEach((region, index) => {
|
|
2317
|
-
const itemCount = Number(region.count || region.length || 0)
|
|
2318
|
-
if (!itemCount) {
|
|
2319
|
-
return
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
const startRatio = consumedCount / total
|
|
2323
|
-
const endRatio = (consumedCount + itemCount) / total
|
|
2324
|
-
const centerRatio = (startRatio + endRatio) / 2
|
|
2325
|
-
const centerX = WATER_LEVEL_CHART_GRID.left + plotWidth * centerRatio
|
|
2326
|
-
const splitX = WATER_LEVEL_CHART_GRID.left + plotWidth * endRatio
|
|
2327
|
-
|
|
2328
|
-
regionGraphics.push({
|
|
2329
|
-
type: 'group',
|
|
2330
|
-
left: centerX - 32,
|
|
2331
|
-
top: 18,
|
|
2332
|
-
children: [
|
|
2333
|
-
{
|
|
2334
|
-
type: 'rect',
|
|
2335
|
-
shape: {
|
|
2336
|
-
x: 0,
|
|
2337
|
-
y: 0,
|
|
2338
|
-
width: 64,
|
|
2339
|
-
height: 18
|
|
2340
|
-
},
|
|
2341
|
-
style: {
|
|
2342
|
-
fill: '#efefef',
|
|
2343
|
-
stroke: '#999',
|
|
2344
|
-
lineWidth: 1
|
|
2345
|
-
}
|
|
2346
|
-
},
|
|
2347
|
-
{
|
|
2348
|
-
type: 'text',
|
|
2349
|
-
style: {
|
|
2350
|
-
x: 32,
|
|
2351
|
-
y: 9,
|
|
2352
|
-
text: region.name || `区域${index + 1}`,
|
|
2353
|
-
fill: '#333',
|
|
2354
|
-
fontSize: 12,
|
|
2355
|
-
fontFamily: 'Microsoft YaHei',
|
|
2356
|
-
textAlign: 'center',
|
|
2357
|
-
textVerticalAlign: 'middle'
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
]
|
|
2361
|
-
})
|
|
2362
|
-
|
|
2363
|
-
if (index < chartConfig.regionList.length - 1) {
|
|
2364
|
-
regionGraphics.push({
|
|
2365
|
-
type: 'line',
|
|
2366
|
-
shape: {
|
|
2367
|
-
x1: splitX,
|
|
2368
|
-
y1: 38,
|
|
2369
|
-
x2: splitX,
|
|
2370
|
-
y2: plotBottom
|
|
2371
|
-
},
|
|
2372
|
-
style: {
|
|
2373
|
-
stroke: '#777',
|
|
2374
|
-
lineWidth: 1
|
|
2375
|
-
}
|
|
2376
|
-
})
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
consumedCount += itemCount
|
|
2380
|
-
})
|
|
2381
|
-
|
|
2382
|
-
return {
|
|
2383
|
-
animation: false,
|
|
2384
|
-
backgroundColor: '#fff',
|
|
2385
|
-
title: {
|
|
2386
|
-
text: chartConfig.title,
|
|
2387
|
-
left: 'center',
|
|
2388
|
-
top: 0,
|
|
2389
|
-
textStyle: {
|
|
2390
|
-
color: '#333',
|
|
2391
|
-
fontSize: 14,
|
|
2392
|
-
fontWeight: 400
|
|
2393
|
-
}
|
|
2394
|
-
},
|
|
2395
|
-
legend: {
|
|
2396
|
-
top: 58,
|
|
2397
|
-
right: 18,
|
|
2398
|
-
itemWidth: 18,
|
|
2399
|
-
itemHeight: 2,
|
|
2400
|
-
textStyle: {
|
|
2401
|
-
color: '#666',
|
|
2402
|
-
fontSize: 11
|
|
2403
|
-
},
|
|
2404
|
-
data: ['合理区间上限', '合理区间下限']
|
|
2405
|
-
},
|
|
2406
|
-
grid: {
|
|
2407
|
-
...WATER_LEVEL_CHART_GRID
|
|
2408
|
-
},
|
|
2409
|
-
tooltip: {
|
|
2410
|
-
trigger: 'axis'
|
|
2411
|
-
},
|
|
2412
|
-
xAxis: {
|
|
2413
|
-
type: 'category',
|
|
2414
|
-
data: chartConfig.xAxisData,
|
|
2415
|
-
axisTick: {
|
|
2416
|
-
show: false
|
|
2417
|
-
},
|
|
2418
|
-
axisLine: {
|
|
2419
|
-
lineStyle: {
|
|
2420
|
-
color: '#888'
|
|
2421
|
-
}
|
|
2422
|
-
},
|
|
2423
|
-
axisLabel: {
|
|
2424
|
-
interval: 0,
|
|
2425
|
-
rotate: 90,
|
|
2426
|
-
color: '#333',
|
|
2427
|
-
fontSize: 11
|
|
2428
|
-
}
|
|
2429
|
-
},
|
|
2430
|
-
yAxis: {
|
|
2431
|
-
type: 'value',
|
|
2432
|
-
min: 0,
|
|
2433
|
-
max: 100,
|
|
2434
|
-
axisLine: {
|
|
2435
|
-
show: true
|
|
2436
|
-
},
|
|
2437
|
-
splitLine: {
|
|
2438
|
-
lineStyle: {
|
|
2439
|
-
color: '#e8e8e8'
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
},
|
|
2443
|
-
series: [
|
|
2444
|
-
{
|
|
2445
|
-
name: '水位值',
|
|
2446
|
-
type: 'bar',
|
|
2447
|
-
barWidth: 26,
|
|
2448
|
-
data: chartConfig.yAxisData.map((value) => {
|
|
2449
|
-
const numericValue = Number(value)
|
|
2450
|
-
let color = '#86c5e3'
|
|
2451
|
-
|
|
2452
|
-
if (numericValue >= chartConfig.upperLimit) {
|
|
2453
|
-
color = '#ff2a23'
|
|
2454
|
-
} else if (numericValue >= chartConfig.upperLimit * 0.85) {
|
|
2455
|
-
color = '#ffef1f'
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
return {
|
|
2459
|
-
value: numericValue,
|
|
2460
|
-
itemStyle: {
|
|
2461
|
-
color
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
}),
|
|
2465
|
-
label: {
|
|
2466
|
-
show: true,
|
|
2467
|
-
position: 'top',
|
|
2468
|
-
color: '#333',
|
|
2469
|
-
fontSize: 11,
|
|
2470
|
-
formatter: ({ value }) => Number(value).toFixed(2)
|
|
2471
|
-
},
|
|
2472
|
-
markLine: {
|
|
2473
|
-
silent: true,
|
|
2474
|
-
symbol: 'none',
|
|
2475
|
-
lineStyle: {
|
|
2476
|
-
width: 1
|
|
2477
|
-
},
|
|
2478
|
-
data: [
|
|
2479
|
-
{
|
|
2480
|
-
name: '合理区间上限',
|
|
2481
|
-
yAxis: chartConfig.upperLimit,
|
|
2482
|
-
lineStyle: {
|
|
2483
|
-
color: '#ff6b6b',
|
|
2484
|
-
type: 'dashed'
|
|
2485
|
-
}
|
|
2486
|
-
},
|
|
2487
|
-
{
|
|
2488
|
-
name: '合理区间下限',
|
|
2489
|
-
yAxis: chartConfig.lowerLimit,
|
|
2490
|
-
lineStyle: {
|
|
2491
|
-
color: '#77c48b',
|
|
2492
|
-
type: 'dashed'
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
2495
|
-
]
|
|
2496
|
-
}
|
|
2497
|
-
}
|
|
2498
|
-
],
|
|
2499
|
-
graphic: regionGraphics
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
async function renderChartToImageBuffer(config = {}, tagName = '') {
|
|
2504
|
-
const chartDom = document.createElement('div')
|
|
2505
|
-
chartDom.style.cssText = `position:fixed;left:-10000px;top:-10000px;width:${WATER_LEVEL_CHART_CANVAS_WIDTH}px;height:${WATER_LEVEL_CHART_CANVAS_HEIGHT}px;background:#fff;`
|
|
2506
|
-
document.body.appendChild(chartDom)
|
|
2507
|
-
|
|
2508
|
-
try {
|
|
2509
|
-
const chart = echarts.init(chartDom, null, {
|
|
2510
|
-
renderer: 'canvas',
|
|
2511
|
-
width: WATER_LEVEL_CHART_CANVAS_WIDTH,
|
|
2512
|
-
height: WATER_LEVEL_CHART_CANVAS_HEIGHT
|
|
2513
|
-
})
|
|
2514
|
-
chart.setOption(buildWaterLevelChartOption(config, tagName))
|
|
2515
|
-
const dataUrl = chart.getDataURL({
|
|
2516
|
-
type: 'png',
|
|
2517
|
-
pixelRatio: 2,
|
|
2518
|
-
backgroundColor: '#fff'
|
|
2519
|
-
})
|
|
2520
|
-
chart.dispose()
|
|
2521
|
-
const response = await fetch(dataUrl)
|
|
2522
|
-
return response.arrayBuffer()
|
|
2523
|
-
} finally {
|
|
2524
|
-
chartDom.remove()
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
async function loadTemplate() {
|
|
2529
|
-
const templateUrl = props.templateUrl || `/${encodeURIComponent(props.templateFileName || DEFAULT_TEMPLATE_FILE_NAME)}`
|
|
2530
|
-
const response = await fetch(templateUrl)
|
|
2531
|
-
|
|
2532
|
-
if (!response.ok) {
|
|
2533
|
-
throw new Error(`模板加载失败: ${response.status}`)
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
return response.arrayBuffer()
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
function stripEditorMarkersFromXml(xmlText = '') {
|
|
2540
|
-
return xmlText
|
|
2541
|
-
.replaceAll(EDITOR_MARKER_START, '')
|
|
2542
|
-
.replaceAll(EDITOR_MARKER_END, '')
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
function createTemplateZip(templateBuffer, options = {}) {
|
|
2546
|
-
const { stripEditorMarkers = false } = options
|
|
2547
|
-
const zip = new PizZip(templateBuffer)
|
|
2548
|
-
const documentXml = zip.file('word/document.xml')?.asText?.() || ''
|
|
2549
|
-
let patchedXml = WATER_LEVEL_CHART_TAGS.reduce((result, key) => {
|
|
2550
|
-
return result.replaceAll(`{${key}}`, `{%${key}}`)
|
|
2551
|
-
}, documentXml)
|
|
2552
|
-
|
|
2553
|
-
if (stripEditorMarkers) {
|
|
2554
|
-
patchedXml = stripEditorMarkersFromXml(patchedXml)
|
|
2555
|
-
}
|
|
2556
|
-
|
|
2557
|
-
if (patchedXml !== documentXml) {
|
|
2558
|
-
zip.file('word/document.xml', patchedXml)
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
|
-
return {
|
|
2562
|
-
zip,
|
|
2563
|
-
documentXml: patchedXml
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
async function createRenderedDocxBlob(options = {}) {
|
|
2568
|
-
const { stripEditorMarkers = true, applyRichText = true } = options
|
|
2569
|
-
const templateBuffer = await loadTemplate()
|
|
2570
|
-
const { zip: templateContextZip, documentXml: templateContextXml } = createTemplateZip(templateBuffer, {
|
|
2571
|
-
stripEditorMarkers: false
|
|
2572
|
-
})
|
|
2573
|
-
const { zip } = createTemplateZip(templateBuffer, { stripEditorMarkers })
|
|
2574
|
-
const currentWordStyleContext = extractWordStyleContext(templateContextZip, templateContextXml)
|
|
2575
|
-
const imageModule = new ImageModule({
|
|
2576
|
-
centered: true,
|
|
2577
|
-
fileType: 'docx',
|
|
2578
|
-
getImage(tagValue, tagName) {
|
|
2579
|
-
if (getChartTagNames()[tagName]) {
|
|
2580
|
-
return renderChartToImageBuffer(tagValue, tagName)
|
|
2581
|
-
}
|
|
2582
|
-
return tagValue
|
|
2583
|
-
},
|
|
2584
|
-
getSize(img, tagValue, tagName) {
|
|
2585
|
-
if (getChartTagNames()[tagName]) {
|
|
2586
|
-
return [WATER_LEVEL_CHART_DOCX_WIDTH, WATER_LEVEL_CHART_DOCX_HEIGHT]
|
|
2587
|
-
}
|
|
2588
|
-
return [120, 120]
|
|
2589
|
-
}
|
|
2590
|
-
})
|
|
2591
|
-
const doc = new Docxtemplater(zip, {
|
|
2592
|
-
modules: [imageModule],
|
|
2593
|
-
paragraphLoop: true,
|
|
2594
|
-
linebreaks: true
|
|
2595
|
-
})
|
|
2596
|
-
const renderData = buildTemplateDataForRender({
|
|
2597
|
-
useRichTextTokens: applyRichText,
|
|
2598
|
-
useRichTextPlainText: applyRichText
|
|
2599
|
-
})
|
|
2600
|
-
|
|
2601
|
-
if (typeof doc.resolveData === 'function') {
|
|
2602
|
-
await doc.resolveData(renderData)
|
|
2603
|
-
} else if (typeof doc.setData === 'function') {
|
|
2604
|
-
doc.setData(renderData)
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
doc.render()
|
|
2608
|
-
if (applyRichText) {
|
|
2609
|
-
applyRichTextTokensToDocx(doc.getZip(), editableRenderData.value.__editableRichTextMap || {}, {
|
|
2610
|
-
wordStyleContext: currentWordStyleContext
|
|
2611
|
-
})
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
return doc.getZip().generate({
|
|
2615
|
-
type: 'blob',
|
|
2616
|
-
mimeType: DOCX_MIME_TYPE
|
|
2617
|
-
})
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
function downloadBlob(blob, fileName) {
|
|
2621
|
-
const downloadUrl = URL.createObjectURL(blob)
|
|
2622
|
-
const anchor = document.createElement('a')
|
|
2623
|
-
anchor.href = downloadUrl
|
|
2624
|
-
anchor.download = fileName
|
|
2625
|
-
document.body.appendChild(anchor)
|
|
2626
|
-
anchor.click()
|
|
2627
|
-
document.body.removeChild(anchor)
|
|
2628
|
-
URL.revokeObjectURL(downloadUrl)
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
function getAllTextNodes(container) {
|
|
2632
|
-
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT)
|
|
2633
|
-
const textNodes = []
|
|
2634
|
-
|
|
2635
|
-
while (walker.nextNode()) {
|
|
2636
|
-
const currentNode = walker.currentNode
|
|
2637
|
-
textNodes.push(currentNode)
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
return textNodes
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
function getNodePositionByOffset(textNodes, absoluteOffset, bias = 'end') {
|
|
2644
|
-
let currentOffset = 0
|
|
2645
|
-
|
|
2646
|
-
for (const node of textNodes) {
|
|
2647
|
-
const textLength = (node.textContent || '').length
|
|
2648
|
-
const nextOffset = currentOffset + textLength
|
|
2649
|
-
|
|
2650
|
-
const isTargetNode = bias === 'start'
|
|
2651
|
-
? absoluteOffset < nextOffset
|
|
2652
|
-
: absoluteOffset <= nextOffset
|
|
2653
|
-
|
|
2654
|
-
if (isTargetNode) {
|
|
2655
|
-
return {
|
|
2656
|
-
node,
|
|
2657
|
-
offset: Math.max(0, absoluteOffset - currentOffset)
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
currentOffset = nextOffset
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
const lastNode = textNodes[textNodes.length - 1]
|
|
2665
|
-
|
|
2666
|
-
return {
|
|
2667
|
-
node: lastNode,
|
|
2668
|
-
offset: (lastNode?.textContent || '').length
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
|
|
2672
|
-
function bindEditableAnchorClick(wrapper, fieldKey) {
|
|
2673
|
-
const onClick = () => openFieldEditor(fieldKey)
|
|
2674
|
-
wrapper.addEventListener('click', onClick)
|
|
2675
|
-
|
|
2676
|
-
editableDomCleanupList.push(() => {
|
|
2677
|
-
wrapper.removeEventListener('click', onClick)
|
|
2678
|
-
})
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
|
-
function createInlineEditableAnchor(fieldKey, fallbackText = '') {
|
|
2682
|
-
const wrapper = document.createElement('span')
|
|
2683
|
-
wrapper.className = 'editable-field-anchor'
|
|
2684
|
-
wrapper.dataset.fieldKey = fieldKey
|
|
2685
|
-
const hasContent = hasEditableDisplayContent(fieldKey, fallbackText)
|
|
2686
|
-
|
|
2687
|
-
if (!hasContent) {
|
|
2688
|
-
wrapper.classList.add('is-placeholder')
|
|
2689
|
-
wrapper.textContent = EDITABLE_EMPTY_PLACEHOLDER
|
|
2690
|
-
bindEditableAnchorClick(wrapper, fieldKey)
|
|
2691
|
-
return wrapper
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
const segments = getRenderableFieldHtml(fieldKey, fallbackText)
|
|
2695
|
-
|
|
2696
|
-
segments.forEach((segment) => {
|
|
2697
|
-
if (segment.type === 'break') {
|
|
2698
|
-
wrapper.appendChild(document.createElement('br'))
|
|
2699
|
-
return
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
wrapper.appendChild(document.createTextNode(segment.text || ''))
|
|
2703
|
-
})
|
|
2704
|
-
|
|
2705
|
-
bindEditableAnchorClick(wrapper, fieldKey)
|
|
2706
|
-
return wrapper
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
function createBlockEditableAnchor(fieldKey, fallbackText = '', sourceParagraph = null) {
|
|
2710
|
-
const wrapper = document.createElement('div')
|
|
2711
|
-
wrapper.className = 'editable-field-block'
|
|
2712
|
-
wrapper.dataset.fieldKey = fieldKey
|
|
2713
|
-
const hasContent = hasEditableDisplayContent(fieldKey, fallbackText)
|
|
2714
|
-
|
|
2715
|
-
if (hasContent) {
|
|
2716
|
-
wrapper.innerHTML = getFieldRenderableHtml(fieldKey, fallbackText)
|
|
2717
|
-
} else {
|
|
2718
|
-
wrapper.classList.add('is-placeholder')
|
|
2719
|
-
wrapper.textContent = EDITABLE_EMPTY_PLACEHOLDER
|
|
2720
|
-
}
|
|
2721
|
-
|
|
2722
|
-
if (wrapper.querySelector('table')) {
|
|
2723
|
-
wrapper.classList.add('is-table-field')
|
|
2724
|
-
}
|
|
2725
|
-
applySourceParagraphLayout(wrapper, sourceParagraph)
|
|
2726
|
-
bindEditableAnchorClick(wrapper, fieldKey)
|
|
2727
|
-
return wrapper
|
|
2728
|
-
}
|
|
2729
|
-
|
|
2730
|
-
function stripEditorMarkersFromElement(element) {
|
|
2731
|
-
if (!element) {
|
|
2732
|
-
return
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
getAllTextNodes(element).forEach((node) => {
|
|
2736
|
-
const text = node.textContent || ''
|
|
2737
|
-
const cleanedText = text
|
|
2738
|
-
.replaceAll(`${LEGACY_EDITOR_MARKER_START}${EDITOR_MARKER_START}`, '')
|
|
2739
|
-
.replaceAll(`${EDITOR_MARKER_END}${LEGACY_EDITOR_MARKER_END}`, '')
|
|
2740
|
-
.replaceAll(`${LEGACY_EDITOR_MARKER_START} ${EDITOR_MARKER_START}`, '')
|
|
2741
|
-
.replaceAll(`${EDITOR_MARKER_END} ${LEGACY_EDITOR_MARKER_END}`, '')
|
|
2742
|
-
.replaceAll(EDITOR_MARKER_START, '')
|
|
2743
|
-
.replaceAll(EDITOR_MARKER_END, '')
|
|
2744
|
-
.replaceAll(LEGACY_EDITOR_MARKER_START, '')
|
|
2745
|
-
.replaceAll(LEGACY_EDITOR_MARKER_END, '')
|
|
2746
|
-
|
|
2747
|
-
if (cleanedText !== text) {
|
|
2748
|
-
node.textContent = cleanedText
|
|
2749
|
-
}
|
|
2750
|
-
})
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
function getCleanOuterHtml(element) {
|
|
2754
|
-
if (!element) {
|
|
2755
|
-
return ''
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
const clonedElement = element.cloneNode(true)
|
|
2759
|
-
stripEditorMarkersFromElement(clonedElement)
|
|
2760
|
-
return clonedElement.outerHTML || ''
|
|
2761
|
-
}
|
|
2762
|
-
|
|
2763
|
-
function getSharedClosestTable(startNode, endNode) {
|
|
2764
|
-
const startElement = startNode?.nodeType === DOM_ELEMENT_NODE ? startNode : startNode?.parentElement
|
|
2765
|
-
const endElement = endNode?.nodeType === DOM_ELEMENT_NODE ? endNode : endNode?.parentElement
|
|
2766
|
-
const startTable = startElement?.closest?.('table') || null
|
|
2767
|
-
const endTable = endElement?.closest?.('table') || null
|
|
2768
|
-
|
|
2769
|
-
return startTable && startTable === endTable ? startTable : null
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
function getRangeContainedTableHtml(startPosition, endPosition) {
|
|
2773
|
-
if (!startPosition.node || !endPosition.node) {
|
|
2774
|
-
return ''
|
|
2775
|
-
}
|
|
2776
|
-
|
|
2777
|
-
const range = document.createRange()
|
|
2778
|
-
range.setStart(startPosition.node, startPosition.offset)
|
|
2779
|
-
range.setEnd(endPosition.node, endPosition.offset)
|
|
2780
|
-
|
|
2781
|
-
const fragment = range.cloneContents()
|
|
2782
|
-
const table = fragment.querySelector?.('table')
|
|
2783
|
-
|
|
2784
|
-
return table ? getCleanOuterHtml(table) : ''
|
|
2785
|
-
}
|
|
2786
|
-
|
|
2787
|
-
function replaceEditableParagraphRange(startNode, endNode, replacement) {
|
|
2788
|
-
const startParagraph = getClosestParagraphElement(startNode)
|
|
2789
|
-
const endParagraph = getClosestParagraphElement(endNode)
|
|
2790
|
-
|
|
2791
|
-
if (!startParagraph || !endParagraph) {
|
|
2792
|
-
return false
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
if (startParagraph === endParagraph) {
|
|
2796
|
-
startParagraph.replaceWith(replacement)
|
|
2797
|
-
return true
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
const range = document.createRange()
|
|
2801
|
-
range.setStartBefore(startParagraph)
|
|
2802
|
-
range.setEndAfter(endParagraph)
|
|
2803
|
-
range.deleteContents()
|
|
2804
|
-
range.insertNode(replacement)
|
|
2805
|
-
return true
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
function getDocxPreviewClassName(styleId = '') {
|
|
2809
|
-
return `docx_${String(styleId || '').replace(/[ .]+/g, '-').replace(/[&]+/g, 'and').toLowerCase()}`
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
function extractHeadingStyleLevelMap(zip) {
|
|
2813
|
-
const stylesXml = zip.file('word/styles.xml')?.asText?.() || ''
|
|
2814
|
-
const result = {}
|
|
2815
|
-
|
|
2816
|
-
for (const match of stylesXml.matchAll(/<w:style\b[\s\S]*?<\/w:style>/g)) {
|
|
2817
|
-
const styleXml = match[0]
|
|
2818
|
-
const type = styleXml.match(/w:type="([^"]+)"/)?.[1] || ''
|
|
2819
|
-
|
|
2820
|
-
if (type !== 'paragraph') {
|
|
2821
|
-
continue
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
const styleId = styleXml.match(/w:styleId="([^"]+)"/)?.[1] || ''
|
|
2825
|
-
const outlineLevel = styleXml.match(/<w:outlineLvl w:val="([^"]+)"/)?.[1]
|
|
2826
|
-
|
|
2827
|
-
if (!styleId || outlineLevel === undefined) {
|
|
2828
|
-
continue
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
const level = Number(outlineLevel) + 1
|
|
2832
|
-
|
|
2833
|
-
if (level >= 1 && level <= 3) {
|
|
2834
|
-
result[getDocxPreviewClassName(styleId)] = level
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
return result
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
function extractHeadingStyleLevelById(zip) {
|
|
2842
|
-
const stylesXml = zip.file('word/styles.xml')?.asText?.() || ''
|
|
2843
|
-
const result = {}
|
|
2844
|
-
|
|
2845
|
-
for (const match of stylesXml.matchAll(/<w:style\b[\s\S]*?<\/w:style>/g)) {
|
|
2846
|
-
const styleXml = match[0]
|
|
2847
|
-
const type = styleXml.match(/w:type="([^"]+)"/)?.[1] || ''
|
|
2848
|
-
|
|
2849
|
-
if (type !== 'paragraph') {
|
|
2850
|
-
continue
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
const styleId = styleXml.match(/w:styleId="([^"]+)"/)?.[1] || ''
|
|
2854
|
-
const outlineLevel = styleXml.match(/<w:outlineLvl w:val="([^"]+)"/)?.[1]
|
|
2855
|
-
|
|
2856
|
-
if (!styleId || outlineLevel === undefined) {
|
|
2857
|
-
continue
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
const level = Number(outlineLevel) + 1
|
|
2861
|
-
|
|
2862
|
-
if (level >= 1 && level <= 3) {
|
|
2863
|
-
result[styleId] = level
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
return result
|
|
2868
|
-
}
|
|
2869
|
-
|
|
2870
|
-
function extractWordStyleContext(zip, documentXml = '') {
|
|
2871
|
-
const stylesXml = zip.file('word/styles.xml')?.asText?.() || ''
|
|
2872
|
-
const headingStyleIds = {}
|
|
2873
|
-
let bodyStyleId = ''
|
|
2874
|
-
|
|
2875
|
-
for (const match of stylesXml.matchAll(/<w:style\b[\s\S]*?<\/w:style>/g)) {
|
|
2876
|
-
const styleXml = match[0]
|
|
2877
|
-
const type = styleXml.match(/w:type="([^"]+)"/)?.[1] || ''
|
|
2878
|
-
|
|
2879
|
-
if (type !== 'paragraph') {
|
|
2880
|
-
continue
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
const styleId = styleXml.match(/w:styleId="([^"]+)"/)?.[1] || ''
|
|
2884
|
-
const styleName = styleXml.match(/<w:name w:val="([^"]+)"/)?.[1] || ''
|
|
2885
|
-
const outlineLevel = styleXml.match(/<w:outlineLvl w:val="([^"]+)"/)?.[1]
|
|
2886
|
-
|
|
2887
|
-
if (styleId && outlineLevel !== undefined) {
|
|
2888
|
-
const level = Number(outlineLevel) + 1
|
|
2889
|
-
|
|
2890
|
-
if (level >= 1 && level <= 3) {
|
|
2891
|
-
headingStyleIds[level] = styleId
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
|
-
if (!bodyStyleId && /^normal$/i.test(styleName)) {
|
|
2896
|
-
bodyStyleId = styleId
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
|
|
2900
|
-
const headingStyleLevelById = extractHeadingStyleLevelById(zip)
|
|
2901
|
-
const editableHeadingCounters = {}
|
|
2902
|
-
const counters = [0, 0, 0]
|
|
2903
|
-
|
|
2904
|
-
for (const match of documentXml.matchAll(/<w:p\b[\s\S]*?<\/w:p>/g)) {
|
|
2905
|
-
const paragraphXml = match[0]
|
|
2906
|
-
const styleId = paragraphXml.match(/<w:pStyle w:val="([^"]+)"/)?.[1] || ''
|
|
2907
|
-
const level = headingStyleLevelById[styleId]
|
|
2908
|
-
const paragraphText = getParagraphText(paragraphXml)
|
|
2909
|
-
|
|
2910
|
-
if (paragraphText.includes(EDITOR_MARKER_START)) {
|
|
2911
|
-
const descriptorList = extractEditableMarkerDescriptors(paragraphXml)
|
|
2912
|
-
|
|
2913
|
-
descriptorList.forEach((descriptor) => {
|
|
2914
|
-
if (descriptor.isValid && descriptor.fieldKey && !editableHeadingCounters[descriptor.fieldKey]) {
|
|
2915
|
-
editableHeadingCounters[descriptor.fieldKey] = [...counters]
|
|
2916
|
-
}
|
|
2917
|
-
})
|
|
2918
|
-
}
|
|
2919
|
-
|
|
2920
|
-
if (level >= 1 && level <= 3) {
|
|
2921
|
-
counters[level - 1] += 1
|
|
2922
|
-
|
|
2923
|
-
for (let index = level; index < counters.length; index += 1) {
|
|
2924
|
-
counters[index] = 0
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
|
|
2929
|
-
return {
|
|
2930
|
-
headingStyleIds,
|
|
2931
|
-
bodyStyleId,
|
|
2932
|
-
editableHeadingCounters
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
function normalizeOutlineText(text = '') {
|
|
2937
|
-
return String(text || '').replace(/\s+/g, '')
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
function getParagraphText(paragraphXml = '') {
|
|
2941
|
-
return [...paragraphXml.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/g)]
|
|
2942
|
-
.map((match) => decodeXmlText(match[1]))
|
|
2943
|
-
.join('')
|
|
2944
|
-
.trim()
|
|
2945
|
-
.replace(/\s+/g, ' ')
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
|
-
function extractDocumentHeadingList(zip, documentXml = '') {
|
|
2949
|
-
const styleLevelMap = extractHeadingStyleLevelById(zip)
|
|
2950
|
-
const result = []
|
|
2951
|
-
|
|
2952
|
-
for (const match of documentXml.matchAll(/<w:p\b[\s\S]*?<\/w:p>/g)) {
|
|
2953
|
-
const paragraphXml = match[0]
|
|
2954
|
-
const styleId = paragraphXml.match(/<w:pStyle w:val="([^"]+)"/)?.[1] || ''
|
|
2955
|
-
const level = styleLevelMap[styleId]
|
|
2956
|
-
|
|
2957
|
-
if (!level) {
|
|
2958
|
-
continue
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
|
-
const text = getParagraphText(paragraphXml)
|
|
2962
|
-
|
|
2963
|
-
if (!text || text.includes(EDITOR_MARKER_START) || text.includes(EDITOR_MARKER_END)) {
|
|
2964
|
-
continue
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
result.push({
|
|
2968
|
-
level,
|
|
2969
|
-
text,
|
|
2970
|
-
normalizedText: normalizeOutlineText(text)
|
|
2971
|
-
})
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
|
-
return result
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
function getOutlineLevel(element) {
|
|
2978
|
-
if (!element) {
|
|
2979
|
-
return 0
|
|
2980
|
-
}
|
|
2981
|
-
|
|
2982
|
-
for (const className of Array.from(element.classList || [])) {
|
|
2983
|
-
const level = headingStyleLevelMap.value[className]
|
|
2984
|
-
|
|
2985
|
-
if (level >= 1 && level <= 3) {
|
|
2986
|
-
return level
|
|
2987
|
-
}
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
const tagName = element.tagName?.toLowerCase?.() || ''
|
|
2991
|
-
|
|
2992
|
-
if (/^h[1-3]$/.test(tagName)) {
|
|
2993
|
-
return Number(tagName.slice(1))
|
|
2994
|
-
}
|
|
2995
|
-
|
|
2996
|
-
return 0
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
function isVisibleOutlineCandidate(element) {
|
|
3000
|
-
const style = window.getComputedStyle(element)
|
|
3001
|
-
const rect = element.getBoundingClientRect()
|
|
3002
|
-
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
|
|
3003
|
-
}
|
|
3004
|
-
|
|
3005
|
-
function getCandidateNormalizedText(element) {
|
|
3006
|
-
return normalizeOutlineText(element?.textContent || '')
|
|
3007
|
-
}
|
|
3008
|
-
|
|
3009
|
-
function findHeadingElement(heading, candidates, searchStartIndex = 0) {
|
|
3010
|
-
const headingText = heading.normalizedText
|
|
3011
|
-
|
|
3012
|
-
if (!headingText) {
|
|
3013
|
-
return { element: null, index: searchStartIndex }
|
|
3014
|
-
}
|
|
3015
|
-
|
|
3016
|
-
const exactMatchedIndex = candidates.findIndex((element, index) => {
|
|
3017
|
-
return index >= searchStartIndex && getCandidateNormalizedText(element) === headingText
|
|
3018
|
-
})
|
|
3019
|
-
|
|
3020
|
-
if (exactMatchedIndex >= 0) {
|
|
3021
|
-
return {
|
|
3022
|
-
element: candidates[exactMatchedIndex],
|
|
3023
|
-
index: exactMatchedIndex
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
const containingMatches = candidates
|
|
3028
|
-
.map((element, index) => ({
|
|
3029
|
-
element,
|
|
3030
|
-
index,
|
|
3031
|
-
text: getCandidateNormalizedText(element)
|
|
3032
|
-
}))
|
|
3033
|
-
.filter((item) => {
|
|
3034
|
-
return item.index >= searchStartIndex
|
|
3035
|
-
&& item.text.includes(headingText)
|
|
3036
|
-
&& item.text.length <= headingText.length + 12
|
|
3037
|
-
})
|
|
3038
|
-
.sort((first, second) => {
|
|
3039
|
-
return first.text.length - second.text.length || first.index - second.index
|
|
3040
|
-
})
|
|
3041
|
-
|
|
3042
|
-
if (containingMatches.length) {
|
|
3043
|
-
return {
|
|
3044
|
-
element: containingMatches[0].element,
|
|
3045
|
-
index: containingMatches[0].index
|
|
3046
|
-
}
|
|
3047
|
-
}
|
|
3048
|
-
|
|
3049
|
-
const looseMatches = candidates
|
|
3050
|
-
.map((element, index) => ({
|
|
3051
|
-
element,
|
|
3052
|
-
index,
|
|
3053
|
-
text: getCandidateNormalizedText(element)
|
|
3054
|
-
}))
|
|
3055
|
-
.filter((item) => {
|
|
3056
|
-
return item.index >= searchStartIndex && item.text.includes(headingText)
|
|
3057
|
-
})
|
|
3058
|
-
.sort((first, second) => {
|
|
3059
|
-
return first.text.length - second.text.length || first.index - second.index
|
|
3060
|
-
})
|
|
3061
|
-
|
|
3062
|
-
if (looseMatches.length) {
|
|
3063
|
-
return {
|
|
3064
|
-
element: looseMatches[0].element,
|
|
3065
|
-
index: looseMatches[0].index
|
|
3066
|
-
}
|
|
3067
|
-
}
|
|
3068
|
-
|
|
3069
|
-
return { element: null, index: searchStartIndex }
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
function getOutlineCandidateElements() {
|
|
3073
|
-
const root = previewRef.value?.querySelector('.docx') || previewRef.value
|
|
3074
|
-
|
|
3075
|
-
if (!root) {
|
|
3076
|
-
return []
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
|
-
const candidates = Array.from(root.querySelectorAll('h1, h2, h3, p, div, span'))
|
|
3080
|
-
.filter((element) => {
|
|
3081
|
-
if (element === root || element.closest('table') || !isVisibleOutlineCandidate(element)) {
|
|
3082
|
-
return false
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
const text = getCandidateNormalizedText(element)
|
|
3086
|
-
return text.length > 0 && text.length <= 120
|
|
3087
|
-
})
|
|
3088
|
-
|
|
3089
|
-
return candidates.filter((element) => {
|
|
3090
|
-
const text = getCandidateNormalizedText(element)
|
|
3091
|
-
const childWithSameText = Array.from(element.children || []).some((child) => {
|
|
3092
|
-
return candidates.includes(child) && getCandidateNormalizedText(child) === text
|
|
3093
|
-
})
|
|
3094
|
-
|
|
3095
|
-
return !childWithSameText
|
|
3096
|
-
})
|
|
3097
|
-
}
|
|
3098
|
-
|
|
3099
|
-
function buildDocumentOutline() {
|
|
3100
|
-
if (!previewRef.value) {
|
|
3101
|
-
documentOutline.value = []
|
|
3102
|
-
return
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
|
-
const candidates = getOutlineCandidateElements()
|
|
3106
|
-
const seenTexts = new Set()
|
|
3107
|
-
const outlineList = []
|
|
3108
|
-
const templateHeadings = documentHeadingList.value || []
|
|
3109
|
-
|
|
3110
|
-
if (templateHeadings.length) {
|
|
3111
|
-
let searchStartIndex = 0
|
|
3112
|
-
|
|
3113
|
-
templateHeadings.forEach((heading) => {
|
|
3114
|
-
const seenKey = `${heading.level}-${heading.normalizedText}`
|
|
3115
|
-
|
|
3116
|
-
if (seenTexts.has(seenKey)) {
|
|
3117
|
-
return
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
const { element, index } = findHeadingElement(heading, candidates, searchStartIndex)
|
|
3121
|
-
|
|
3122
|
-
if (element) {
|
|
3123
|
-
searchStartIndex = index + 1
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
seenTexts.add(seenKey)
|
|
3127
|
-
const id = `docx-outline-${outlineList.length + 1}`
|
|
3128
|
-
|
|
3129
|
-
if (element) {
|
|
3130
|
-
element.dataset.outlineId = id
|
|
3131
|
-
}
|
|
3132
|
-
|
|
3133
|
-
outlineList.push({
|
|
3134
|
-
id,
|
|
3135
|
-
level: heading.level,
|
|
3136
|
-
text: heading.text,
|
|
3137
|
-
element
|
|
3138
|
-
})
|
|
3139
|
-
})
|
|
3140
|
-
|
|
3141
|
-
documentOutline.value = outlineList
|
|
3142
|
-
activeOutlineId.value = outlineList[0]?.id || ''
|
|
3143
|
-
updateActiveOutlineByScroll()
|
|
3144
|
-
return
|
|
3145
|
-
}
|
|
3146
|
-
|
|
3147
|
-
candidates.forEach((element) => {
|
|
3148
|
-
const text = (element.textContent || '').trim().replace(/\s+/g, ' ')
|
|
3149
|
-
|
|
3150
|
-
if (!text || text.length > 80 || text.includes(EDITOR_MARKER_START) || text.includes(EDITOR_MARKER_END)) {
|
|
3151
|
-
return
|
|
3152
|
-
}
|
|
3153
|
-
|
|
3154
|
-
const level = getOutlineLevel(element)
|
|
3155
|
-
|
|
3156
|
-
if (!level) {
|
|
3157
|
-
return
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
|
-
const seenKey = `${level}-${text}`
|
|
3161
|
-
|
|
3162
|
-
if (seenTexts.has(seenKey)) {
|
|
3163
|
-
return
|
|
3164
|
-
}
|
|
3165
|
-
|
|
3166
|
-
seenTexts.add(seenKey)
|
|
3167
|
-
const id = `docx-outline-${outlineList.length + 1}`
|
|
3168
|
-
element.dataset.outlineId = id
|
|
3169
|
-
outlineList.push({
|
|
3170
|
-
id,
|
|
3171
|
-
level,
|
|
3172
|
-
text,
|
|
3173
|
-
element
|
|
3174
|
-
})
|
|
3175
|
-
})
|
|
3176
|
-
|
|
3177
|
-
documentOutline.value = outlineList
|
|
3178
|
-
activeOutlineId.value = outlineList[0]?.id || ''
|
|
3179
|
-
updateActiveOutlineByScroll()
|
|
3180
|
-
}
|
|
3181
|
-
|
|
3182
|
-
function getDocumentScrollContainer() {
|
|
3183
|
-
return docxScrollRef.value || previewRef.value?.parentElement || null
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
function resolveOutlineElement(item) {
|
|
3187
|
-
if (!item || !previewRef.value) {
|
|
3188
|
-
return null
|
|
3189
|
-
}
|
|
3190
|
-
|
|
3191
|
-
if (item.element?.isConnected) {
|
|
3192
|
-
return item.element
|
|
3193
|
-
}
|
|
3194
|
-
|
|
3195
|
-
const elementById = previewRef.value.querySelector(`[data-outline-id="${item.id}"]`)
|
|
3196
|
-
|
|
3197
|
-
if (elementById) {
|
|
3198
|
-
item.element = elementById
|
|
3199
|
-
return elementById
|
|
3200
|
-
}
|
|
3201
|
-
|
|
3202
|
-
const candidates = getOutlineCandidateElements()
|
|
3203
|
-
const matched = findHeadingElement({
|
|
3204
|
-
normalizedText: normalizeOutlineText(item.text)
|
|
3205
|
-
}, candidates, 0).element
|
|
3206
|
-
|
|
3207
|
-
if (matched) {
|
|
3208
|
-
matched.dataset.outlineId = item.id
|
|
3209
|
-
item.element = matched
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
|
-
return matched
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
|
-
function getElementTopInScrollContainer(element, container) {
|
|
3216
|
-
const elementRect = element.getBoundingClientRect()
|
|
3217
|
-
const containerRect = container.getBoundingClientRect()
|
|
3218
|
-
return elementRect.top - containerRect.top + container.scrollTop
|
|
3219
|
-
}
|
|
3220
|
-
|
|
3221
|
-
function scrollToOutlineItem(item) {
|
|
3222
|
-
const container = getDocumentScrollContainer()
|
|
3223
|
-
const element = resolveOutlineElement(item)
|
|
3224
|
-
|
|
3225
|
-
if (!element || !container) {
|
|
3226
|
-
return
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
|
-
activeOutlineId.value = item.id
|
|
3230
|
-
container.scrollTo({
|
|
3231
|
-
top: Math.max(getElementTopInScrollContainer(element, container) - 24, 0),
|
|
3232
|
-
behavior: 'smooth'
|
|
3233
|
-
})
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
|
-
function updateActiveOutlineByScroll() {
|
|
3237
|
-
const container = getDocumentScrollContainer()
|
|
3238
|
-
|
|
3239
|
-
if (!container || !documentOutline.value.length) {
|
|
3240
|
-
return
|
|
3241
|
-
}
|
|
3242
|
-
|
|
3243
|
-
const containerRect = container.getBoundingClientRect()
|
|
3244
|
-
const anchorTop = containerRect.top + 96
|
|
3245
|
-
let activeItem = null
|
|
3246
|
-
let firstBelowItem = null
|
|
3247
|
-
|
|
3248
|
-
documentOutline.value.forEach((item) => {
|
|
3249
|
-
const element = resolveOutlineElement(item)
|
|
3250
|
-
|
|
3251
|
-
if (!element) {
|
|
3252
|
-
return
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
const rect = element.getBoundingClientRect()
|
|
3256
|
-
|
|
3257
|
-
if (rect.top <= anchorTop) {
|
|
3258
|
-
activeItem = item
|
|
3259
|
-
return
|
|
3260
|
-
}
|
|
3261
|
-
|
|
3262
|
-
if (!firstBelowItem) {
|
|
3263
|
-
firstBelowItem = item
|
|
3264
|
-
}
|
|
3265
|
-
})
|
|
3266
|
-
|
|
3267
|
-
const nextActiveId = activeItem?.id || firstBelowItem?.id || documentOutline.value[0]?.id || ''
|
|
3268
|
-
|
|
3269
|
-
if (nextActiveId && activeOutlineId.value !== nextActiveId) {
|
|
3270
|
-
activeOutlineId.value = nextActiveId
|
|
3271
|
-
}
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
function handleDocumentScroll() {
|
|
3275
|
-
if (outlineScrollRaf) {
|
|
3276
|
-
cancelAnimationFrame(outlineScrollRaf)
|
|
3277
|
-
}
|
|
3278
|
-
|
|
3279
|
-
outlineScrollRaf = requestAnimationFrame(() => {
|
|
3280
|
-
outlineScrollRaf = 0
|
|
3281
|
-
updateActiveOutlineByScroll()
|
|
3282
|
-
})
|
|
3283
|
-
}
|
|
3284
|
-
|
|
3285
|
-
function decorateEditableRegions() {
|
|
3286
|
-
if (props.renderType !== 'edit' || !previewRef.value || !editableMarkerDescriptors.value.length) {
|
|
3287
|
-
return
|
|
3288
|
-
}
|
|
3289
|
-
|
|
3290
|
-
clearEditableDecorations()
|
|
3291
|
-
|
|
3292
|
-
const textNodes = getAllTextNodes(previewRef.value)
|
|
3293
|
-
const combinedText = textNodes.map((node) => node.textContent || '').join('')
|
|
3294
|
-
const markerPairs = [...combinedText.matchAll(getRenderedEditableMarkerRegExp())]
|
|
3295
|
-
.map((match, index) => ({
|
|
3296
|
-
match,
|
|
3297
|
-
descriptor: editableMarkerDescriptors.value[index]
|
|
3298
|
-
}))
|
|
3299
|
-
.filter((item) => item.descriptor?.isValid && item.descriptor?.fieldKey)
|
|
3300
|
-
const replacedTables = new WeakSet()
|
|
3301
|
-
|
|
3302
|
-
for (let index = markerPairs.length - 1; index >= 0; index -= 1) {
|
|
3303
|
-
const { match, descriptor } = markerPairs[index]
|
|
3304
|
-
const fullText = match[0] || ''
|
|
3305
|
-
const fieldKey = descriptor.fieldKey
|
|
3306
|
-
|
|
3307
|
-
if (!fieldKey) {
|
|
3308
|
-
continue
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
const rangeStart = match.index || 0
|
|
3312
|
-
const rangeEnd = rangeStart + fullText.length
|
|
3313
|
-
const fallbackText = (match[1] || '').trim()
|
|
3314
|
-
|
|
3315
|
-
const startPosition = getNodePositionByOffset(textNodes, rangeStart, 'start')
|
|
3316
|
-
const endPosition = getNodePositionByOffset(textNodes, rangeEnd, 'end')
|
|
3317
|
-
|
|
3318
|
-
if (!startPosition.node || !endPosition.node) {
|
|
3319
|
-
continue
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
if (!startPosition.node.isConnected || !endPosition.node.isConnected) {
|
|
3323
|
-
continue
|
|
3324
|
-
}
|
|
3325
|
-
|
|
3326
|
-
const storedRichItem = getStoredRichTextItem(fieldKey)
|
|
3327
|
-
const storedTableHtml = hasRichTextTableBlock(storedRichItem)
|
|
3328
|
-
? getRichTextFirstTableHtml(storedRichItem)
|
|
3329
|
-
: ''
|
|
3330
|
-
|
|
3331
|
-
if (storedTableHtml) {
|
|
3332
|
-
editableFieldTemplateHtmlMap[fieldKey] = storedTableHtml
|
|
3333
|
-
const range = document.createRange()
|
|
3334
|
-
range.setStart(startPosition.node, startPosition.offset)
|
|
3335
|
-
range.setEnd(endPosition.node, endPosition.offset)
|
|
3336
|
-
range.deleteContents()
|
|
3337
|
-
range.insertNode(createBlockEditableAnchor(fieldKey, fallbackText, getClosestParagraphElement(startPosition.node)))
|
|
3338
|
-
continue
|
|
3339
|
-
}
|
|
3340
|
-
|
|
3341
|
-
const containedTableHtml = getRangeContainedTableHtml(startPosition, endPosition)
|
|
3342
|
-
|
|
3343
|
-
if (containedTableHtml) {
|
|
3344
|
-
editableFieldTemplateHtmlMap[fieldKey] = containedTableHtml
|
|
3345
|
-
const range = document.createRange()
|
|
3346
|
-
range.setStart(startPosition.node, startPosition.offset)
|
|
3347
|
-
range.setEnd(endPosition.node, endPosition.offset)
|
|
3348
|
-
range.deleteContents()
|
|
3349
|
-
range.insertNode(createBlockEditableAnchor(fieldKey, fallbackText, getClosestParagraphElement(startPosition.node)))
|
|
3350
|
-
continue
|
|
3351
|
-
}
|
|
3352
|
-
|
|
3353
|
-
const sharedTable = getSharedClosestTable(startPosition.node, endPosition.node)
|
|
3354
|
-
|
|
3355
|
-
if (sharedTable && !replacedTables.has(sharedTable)) {
|
|
3356
|
-
editableFieldTemplateHtmlMap[fieldKey] = getCleanOuterHtml(sharedTable)
|
|
3357
|
-
sharedTable.replaceWith(createBlockEditableAnchor(fieldKey, fallbackText, getClosestParagraphElement(startPosition.node)))
|
|
3358
|
-
replacedTables.add(sharedTable)
|
|
3359
|
-
continue
|
|
3360
|
-
}
|
|
3361
|
-
|
|
3362
|
-
const range = document.createRange()
|
|
3363
|
-
const isStandaloneBlock = isStandaloneParagraphPlaceholder(
|
|
3364
|
-
startPosition,
|
|
3365
|
-
endPosition,
|
|
3366
|
-
fullText,
|
|
3367
|
-
combinedText,
|
|
3368
|
-
rangeStart,
|
|
3369
|
-
rangeEnd
|
|
3370
|
-
)
|
|
3371
|
-
|
|
3372
|
-
if (isStandaloneBlock) {
|
|
3373
|
-
const paragraphElement = getClosestParagraphElement(startPosition.node)
|
|
3374
|
-
replaceEditableParagraphRange(
|
|
3375
|
-
startPosition.node,
|
|
3376
|
-
endPosition.node,
|
|
3377
|
-
createBlockEditableAnchor(fieldKey, fallbackText, paragraphElement)
|
|
3378
|
-
)
|
|
3379
|
-
continue
|
|
3380
|
-
}
|
|
3381
|
-
|
|
3382
|
-
range.setStart(startPosition.node, startPosition.offset)
|
|
3383
|
-
range.setEnd(endPosition.node, endPosition.offset)
|
|
3384
|
-
range.deleteContents()
|
|
3385
|
-
range.insertNode(createInlineEditableAnchor(fieldKey, fallbackText))
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
|
|
3389
|
-
function openFieldEditor(fieldKey) {
|
|
3390
|
-
const path = editableFieldPathMap.value[fieldKey] || ''
|
|
3391
|
-
const plainText = getValueByPath(editableRenderData.value, path) ?? ''
|
|
3392
|
-
|
|
3393
|
-
activeEditableField.value = {
|
|
3394
|
-
key: fieldKey,
|
|
3395
|
-
path
|
|
3396
|
-
}
|
|
3397
|
-
|
|
3398
|
-
editor.value?.commands.setContent(getFieldStoredHtml(fieldKey, plainText), false)
|
|
3399
|
-
editDialogVisible.value = true
|
|
3400
|
-
}
|
|
3401
|
-
|
|
3402
|
-
function saveActiveField() {
|
|
3403
|
-
if (!activeEditableField.value || !editor.value) {
|
|
3404
|
-
return
|
|
3405
|
-
}
|
|
3406
|
-
|
|
3407
|
-
const fieldKey = activeEditableField.value.key
|
|
3408
|
-
const html = normalizeRichTextHtmlForStorage(editor.value.getHTML())
|
|
3409
|
-
const json = editor.value.getJSON()
|
|
3410
|
-
const plainText = htmlToDocxText(html)
|
|
3411
|
-
const path = activeEditableField.value.path
|
|
3412
|
-
|
|
3413
|
-
editableRenderData.value.__editableRichTextMap[fieldKey] = buildBackendRichTextValue(
|
|
3414
|
-
fieldKey,
|
|
3415
|
-
path,
|
|
3416
|
-
html,
|
|
3417
|
-
json,
|
|
3418
|
-
plainText
|
|
3419
|
-
)
|
|
3420
|
-
|
|
3421
|
-
setValueByPath(editableRenderData.value, path, plainText)
|
|
3422
|
-
editableFieldContentMap[fieldKey] = html
|
|
3423
|
-
|
|
3424
|
-
emit('render-data-change', deepClone(editableRenderData.value))
|
|
3425
|
-
emit('save-payload-change', buildSavePayload())
|
|
3426
|
-
editDialogVisible.value = false
|
|
3427
|
-
renderPreview()
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
|
-
function onDialogClosed() {
|
|
3431
|
-
activeEditableField.value = null
|
|
3432
|
-
}
|
|
3433
|
-
|
|
3434
|
-
async function renderPreview() {
|
|
3435
|
-
const currentToken = ++renderToken
|
|
3436
|
-
isLoading.value = true
|
|
3437
|
-
errorMessage.value = ''
|
|
3438
|
-
|
|
3439
|
-
try {
|
|
3440
|
-
const templateBuffer = await loadTemplate()
|
|
3441
|
-
const { zip, documentXml } = createTemplateZip(templateBuffer, {
|
|
3442
|
-
stripEditorMarkers: props.renderType !== 'edit'
|
|
3443
|
-
})
|
|
3444
|
-
|
|
3445
|
-
headingStyleLevelMap.value = extractHeadingStyleLevelMap(zip)
|
|
3446
|
-
documentHeadingList.value = extractDocumentHeadingList(zip, documentXml)
|
|
3447
|
-
wordStyleContext.value = extractWordStyleContext(zip, documentXml)
|
|
3448
|
-
editableMarkerDescriptors.value = extractEditableMarkerDescriptors(documentXml)
|
|
3449
|
-
editableFieldKeys.value = editableMarkerDescriptors.value
|
|
3450
|
-
.filter((descriptor) => descriptor.isValid)
|
|
3451
|
-
.map((descriptor) => descriptor.fieldKey)
|
|
3452
|
-
editableFieldPathMap.value = buildScalarPathMap(editableRenderData.value)
|
|
3453
|
-
editableFieldKeys.value.forEach((fieldKey) => {
|
|
3454
|
-
if (!editableFieldPathMap.value[fieldKey]) {
|
|
3455
|
-
editableFieldPathMap.value[fieldKey] = fieldKey
|
|
3456
|
-
}
|
|
3457
|
-
})
|
|
3458
|
-
const renderedBlob = await createRenderedDocxBlob({
|
|
3459
|
-
stripEditorMarkers: props.renderType !== 'edit',
|
|
3460
|
-
applyRichText: props.renderType !== 'edit'
|
|
3461
|
-
})
|
|
3462
|
-
|
|
3463
|
-
await nextTick()
|
|
3464
|
-
|
|
3465
|
-
if (!previewRef.value || currentToken !== renderToken) {
|
|
3466
|
-
return
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
|
-
clearPreview()
|
|
3470
|
-
|
|
3471
|
-
await docxPreview.renderAsync(renderedBlob, previewRef.value, previewRef.value, {
|
|
3472
|
-
className: 'docx-preview',
|
|
3473
|
-
inWrapper: true,
|
|
3474
|
-
breakPages: true,
|
|
3475
|
-
ignoreWidth: false,
|
|
3476
|
-
ignoreHeight: false,
|
|
3477
|
-
ignoreLastRenderedPageBreak: false,
|
|
3478
|
-
renderHeaders: true,
|
|
3479
|
-
renderFooters: true,
|
|
3480
|
-
useBase64URL: true
|
|
3481
|
-
})
|
|
3482
|
-
|
|
3483
|
-
await nextTick()
|
|
3484
|
-
|
|
3485
|
-
if (props.renderType === 'edit') {
|
|
3486
|
-
decorateEditableRegions()
|
|
3487
|
-
} else {
|
|
3488
|
-
cleanupEditorMarkers()
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
buildDocumentOutline()
|
|
3492
|
-
} catch (error) {
|
|
3493
|
-
if (currentToken !== renderToken) {
|
|
3494
|
-
return
|
|
3495
|
-
}
|
|
3496
|
-
|
|
3497
|
-
console.error('Docx preview render failed:', error)
|
|
3498
|
-
errorMessage.value = '文档预览生成失败,请检查模板变量或模板文件。'
|
|
3499
|
-
ElMessage.error(errorMessage.value)
|
|
3500
|
-
} finally {
|
|
3501
|
-
if (currentToken === renderToken) {
|
|
3502
|
-
isLoading.value = false
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
async function exportDocx() {
|
|
3508
|
-
try {
|
|
3509
|
-
const renderedBlob = await createRenderedDocxBlob({
|
|
3510
|
-
stripEditorMarkers: true
|
|
3511
|
-
})
|
|
3512
|
-
downloadBlob(renderedBlob, getExportFileName())
|
|
3513
|
-
return {
|
|
3514
|
-
success: true,
|
|
3515
|
-
fileName: getExportFileName()
|
|
3516
|
-
}
|
|
3517
|
-
} catch (error) {
|
|
3518
|
-
console.error('Docx export failed:', error)
|
|
3519
|
-
ElMessage.error('导出失败,请检查模板变量或模板文件。')
|
|
3520
|
-
return {
|
|
3521
|
-
success: false,
|
|
3522
|
-
error
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
function isHeadingActive(level) {
|
|
3528
|
-
return !!editor.value?.isActive('heading', { level })
|
|
3529
|
-
}
|
|
3530
|
-
|
|
3531
|
-
function isTextAlignActive(alignment) {
|
|
3532
|
-
return !!editor.value?.isActive({ textAlign: alignment })
|
|
3533
|
-
}
|
|
3534
|
-
|
|
3535
|
-
function setHeading(level) {
|
|
3536
|
-
editor.value?.chain().focus().toggleHeading({ level }).run()
|
|
3537
|
-
}
|
|
3538
|
-
|
|
3539
|
-
function setParagraph() {
|
|
3540
|
-
editor.value?.chain().focus().setParagraph().updateAttributes('paragraph', {
|
|
3541
|
-
textIndentLevel: 0
|
|
3542
|
-
}).setTextAlign('left').run()
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
function toggleBold() {
|
|
3546
|
-
editor.value?.chain().focus().toggleBold().run()
|
|
3547
|
-
}
|
|
3548
|
-
|
|
3549
|
-
function getCurrentEditorFormat() {
|
|
3550
|
-
if (!editor.value) {
|
|
3551
|
-
return null
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
const headingLevel = headingActions.find((item) => editor.value.isActive('heading', { level: item.level }))?.level || 0
|
|
3555
|
-
|
|
3556
|
-
return {
|
|
3557
|
-
headingLevel,
|
|
3558
|
-
bold: Boolean(editor.value.isActive('bold')),
|
|
3559
|
-
textColor: editor.value.getAttributes('textStyle').color || '',
|
|
3560
|
-
backgroundColor: editor.value.getAttributes('highlight').color || '',
|
|
3561
|
-
align: ['center', 'right'].find((alignment) => editor.value.isActive({ textAlign: alignment })) || 'left',
|
|
3562
|
-
textIndentLevel: getEditorIndentLevel(editor.value)
|
|
3563
|
-
}
|
|
3564
|
-
}
|
|
3565
|
-
|
|
3566
|
-
function applyEditorFormat(format) {
|
|
3567
|
-
if (!editor.value || !format) {
|
|
3568
|
-
return
|
|
3569
|
-
}
|
|
3570
|
-
|
|
3571
|
-
let command = editor.value.chain().focus().unsetAllMarks()
|
|
3572
|
-
|
|
3573
|
-
if (format.headingLevel) {
|
|
3574
|
-
command = command.setHeading({ level: format.headingLevel })
|
|
3575
|
-
} else {
|
|
3576
|
-
command = command.setParagraph()
|
|
3577
|
-
}
|
|
3578
|
-
|
|
3579
|
-
command = command
|
|
3580
|
-
.setTextAlign(format.align || 'left')
|
|
3581
|
-
.updateAttributes(format.headingLevel ? 'heading' : 'paragraph', {
|
|
3582
|
-
textIndentLevel: Number(format.textIndentLevel || 0)
|
|
3583
|
-
})
|
|
3584
|
-
|
|
3585
|
-
if (format.bold) {
|
|
3586
|
-
command = command.toggleBold()
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
if (format.textColor) {
|
|
3590
|
-
command = command.setColor(format.textColor)
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3593
|
-
if (format.backgroundColor) {
|
|
3594
|
-
command = command.setHighlight({ color: format.backgroundColor })
|
|
3595
|
-
}
|
|
3596
|
-
|
|
3597
|
-
command.run()
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
function toggleFormatPainter() {
|
|
3601
|
-
if (!copiedFormat.value) {
|
|
3602
|
-
copiedFormat.value = getCurrentEditorFormat()
|
|
3603
|
-
ElMessage.success('已复制当前格式,请选中目标内容后再次点击格式刷应用。')
|
|
3604
|
-
return
|
|
3605
|
-
}
|
|
3606
|
-
|
|
3607
|
-
applyEditorFormat(copiedFormat.value)
|
|
3608
|
-
copiedFormat.value = null
|
|
3609
|
-
}
|
|
3610
|
-
|
|
3611
|
-
function clearEditorFormat() {
|
|
3612
|
-
if (!editor.value) {
|
|
3613
|
-
return
|
|
3614
|
-
}
|
|
3615
|
-
|
|
3616
|
-
editor.value
|
|
3617
|
-
.chain()
|
|
3618
|
-
.focus()
|
|
3619
|
-
.unsetAllMarks()
|
|
3620
|
-
.setParagraph()
|
|
3621
|
-
.setTextAlign('left')
|
|
3622
|
-
.updateAttributes('paragraph', {
|
|
3623
|
-
textIndentLevel: 0
|
|
3624
|
-
})
|
|
3625
|
-
.run()
|
|
3626
|
-
}
|
|
3627
|
-
|
|
3628
|
-
function onTablePickerHover(rows, cols) {
|
|
3629
|
-
tablePickerHover.value = { rows, cols }
|
|
3630
|
-
}
|
|
3631
|
-
|
|
3632
|
-
function insertTable(rows, cols) {
|
|
3633
|
-
editor.value?.chain().focus().insertTable({
|
|
3634
|
-
rows,
|
|
3635
|
-
cols,
|
|
3636
|
-
withHeaderRow: true
|
|
3637
|
-
}).run()
|
|
3638
|
-
tablePickerVisible.value = false
|
|
3639
|
-
}
|
|
3640
|
-
|
|
3641
|
-
function addTableRow() {
|
|
3642
|
-
editor.value?.chain().focus().addRowAfter().run()
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
function deleteTableRow() {
|
|
3646
|
-
editor.value?.chain().focus().deleteRow().run()
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
function addTableColumn() {
|
|
3650
|
-
editor.value?.chain().focus().addColumnAfter().run()
|
|
3651
|
-
}
|
|
3652
|
-
|
|
3653
|
-
function deleteTableColumn() {
|
|
3654
|
-
editor.value?.chain().focus().deleteColumn().run()
|
|
3655
|
-
}
|
|
3656
|
-
|
|
3657
|
-
function removeTable() {
|
|
3658
|
-
editor.value?.chain().focus().deleteTable().run()
|
|
3659
|
-
}
|
|
3660
|
-
|
|
3661
|
-
function hideTableContextMenu() {
|
|
3662
|
-
tableContextMenuVisible.value = false
|
|
3663
|
-
}
|
|
3664
|
-
|
|
3665
|
-
function isTableCellSelection(selection) {
|
|
3666
|
-
return selection instanceof CellSelection
|
|
3667
|
-
}
|
|
3668
|
-
|
|
3669
|
-
function shouldKeepTableCellSelection(tableCell) {
|
|
3670
|
-
const selection = editor.value?.state.selection
|
|
3671
|
-
|
|
3672
|
-
return isTableCellSelection(selection) && tableCell.classList.contains('selectedCell')
|
|
3673
|
-
}
|
|
3674
|
-
|
|
3675
|
-
function setEditorSelectionFromEvent(event) {
|
|
3676
|
-
const view = editor.value?.view
|
|
3677
|
-
const position = view?.posAtCoords({
|
|
3678
|
-
left: event.clientX,
|
|
3679
|
-
top: event.clientY
|
|
3680
|
-
})
|
|
3681
|
-
|
|
3682
|
-
if (position?.pos) {
|
|
3683
|
-
editor.value.chain().focus().setTextSelection(position.pos).run()
|
|
3684
|
-
return
|
|
3685
|
-
}
|
|
3686
|
-
|
|
3687
|
-
editor.value?.commands.focus()
|
|
3688
|
-
}
|
|
3689
|
-
|
|
3690
|
-
function onEditorMouseDownCapture(event) {
|
|
3691
|
-
if (event.button !== 2) {
|
|
3692
|
-
return
|
|
3693
|
-
}
|
|
3694
|
-
|
|
3695
|
-
const tableCell = event.target?.closest?.('td, th')
|
|
3696
|
-
|
|
3697
|
-
if (!tableCell || !shouldKeepTableCellSelection(tableCell)) {
|
|
3698
|
-
return
|
|
3699
|
-
}
|
|
3700
|
-
|
|
3701
|
-
event.preventDefault()
|
|
3702
|
-
event.stopPropagation()
|
|
3703
|
-
event.stopImmediatePropagation?.()
|
|
3704
|
-
}
|
|
3705
|
-
|
|
3706
|
-
function onEditorContextMenu(event) {
|
|
3707
|
-
const tableCell = event.target?.closest?.('td, th')
|
|
3708
|
-
|
|
3709
|
-
if (!tableCell || !editor.value) {
|
|
3710
|
-
hideTableContextMenu()
|
|
3711
|
-
return
|
|
3712
|
-
}
|
|
3713
|
-
|
|
3714
|
-
event.preventDefault()
|
|
3715
|
-
|
|
3716
|
-
if (!shouldKeepTableCellSelection(tableCell)) {
|
|
3717
|
-
setEditorSelectionFromEvent(event)
|
|
3718
|
-
}
|
|
3719
|
-
|
|
3720
|
-
const editDialogRect = event.currentTarget.closest('.edit-dialog')?.getBoundingClientRect() || event.currentTarget.getBoundingClientRect()
|
|
3721
|
-
tableContextMenuPosition.value = {
|
|
3722
|
-
x: event.clientX - editDialogRect.left,
|
|
3723
|
-
y: event.clientY - editDialogRect.top
|
|
3724
|
-
}
|
|
3725
|
-
tableContextMenuVisible.value = true
|
|
3726
|
-
}
|
|
3727
|
-
|
|
3728
|
-
function getTableCommandUnavailableMessage(commandName) {
|
|
3729
|
-
if (commandName === 'mergeCells') {
|
|
3730
|
-
return '请先拖选两个或多个连续单元格,再执行合并。'
|
|
3731
|
-
}
|
|
3732
|
-
|
|
3733
|
-
if (commandName === 'splitCell') {
|
|
3734
|
-
return '当前单元格没有可拆分的合并行或合并列。'
|
|
3735
|
-
}
|
|
3736
|
-
|
|
3737
|
-
return ''
|
|
3738
|
-
}
|
|
3739
|
-
|
|
3740
|
-
function runTableMenuCommand(commandName) {
|
|
3741
|
-
const instance = editor.value
|
|
3742
|
-
|
|
3743
|
-
if (!instance) {
|
|
3744
|
-
return
|
|
3745
|
-
}
|
|
3746
|
-
|
|
3747
|
-
const commandMap = {
|
|
3748
|
-
mergeCells: () => instance.chain().focus().mergeCells().run(),
|
|
3749
|
-
splitCell: () => instance.chain().focus().splitCell().run(),
|
|
3750
|
-
addRowBefore: () => instance.chain().focus().addRowBefore().run(),
|
|
3751
|
-
addRowAfter: () => instance.chain().focus().addRowAfter().run(),
|
|
3752
|
-
deleteRow: () => instance.chain().focus().deleteRow().run(),
|
|
3753
|
-
addColumnBefore: () => instance.chain().focus().addColumnBefore().run(),
|
|
3754
|
-
addColumnAfter: () => instance.chain().focus().addColumnAfter().run(),
|
|
3755
|
-
deleteColumn: () => instance.chain().focus().deleteColumn().run(),
|
|
3756
|
-
toggleHeaderRow: () => instance.chain().focus().toggleHeaderRow().run(),
|
|
3757
|
-
toggleHeaderColumn: () => instance.chain().focus().toggleHeaderColumn().run(),
|
|
3758
|
-
deleteTable: () => instance.chain().focus().deleteTable().run()
|
|
3759
|
-
}
|
|
3760
|
-
|
|
3761
|
-
const isCommandAvailable = instance.can()?.[commandName]?.()
|
|
3762
|
-
|
|
3763
|
-
if (isCommandAvailable === false) {
|
|
3764
|
-
const message = getTableCommandUnavailableMessage(commandName)
|
|
3765
|
-
|
|
3766
|
-
if (message) {
|
|
3767
|
-
ElMessage.warning(message)
|
|
3768
|
-
}
|
|
3769
|
-
|
|
3770
|
-
hideTableContextMenu()
|
|
3771
|
-
return
|
|
3772
|
-
}
|
|
3773
|
-
|
|
3774
|
-
const commandResult = commandMap[commandName]?.()
|
|
3775
|
-
|
|
3776
|
-
if (commandResult === false) {
|
|
3777
|
-
const message = getTableCommandUnavailableMessage(commandName)
|
|
3778
|
-
|
|
3779
|
-
if (message) {
|
|
3780
|
-
ElMessage.warning(message)
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
|
|
3784
|
-
hideTableContextMenu()
|
|
3785
|
-
}
|
|
3786
|
-
|
|
3787
|
-
function setTextAlign(alignment) {
|
|
3788
|
-
editor.value?.chain().focus().setTextAlign(alignment).run()
|
|
3789
|
-
}
|
|
3790
|
-
|
|
3791
|
-
function rememberEditorSelection() {
|
|
3792
|
-
const selection = editor.value?.state.selection
|
|
3793
|
-
|
|
3794
|
-
if (!selection) {
|
|
3795
|
-
return
|
|
3796
|
-
}
|
|
3797
|
-
|
|
3798
|
-
pendingEditorSelection.value = {
|
|
3799
|
-
from: selection.from,
|
|
3800
|
-
to: selection.to
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
|
|
3804
|
-
function getEditorCommandWithSelection() {
|
|
3805
|
-
const command = editor.value?.chain().focus()
|
|
3806
|
-
|
|
3807
|
-
if (!command) {
|
|
3808
|
-
return null
|
|
3809
|
-
}
|
|
3810
|
-
|
|
3811
|
-
if (pendingEditorSelection.value) {
|
|
3812
|
-
return command.setTextSelection(pendingEditorSelection.value)
|
|
3813
|
-
}
|
|
3814
|
-
|
|
3815
|
-
return command
|
|
3816
|
-
}
|
|
3817
|
-
|
|
3818
|
-
function openTextColorPicker() {
|
|
3819
|
-
rememberEditorSelection()
|
|
3820
|
-
textColorInputRef.value?.click()
|
|
3821
|
-
}
|
|
3822
|
-
|
|
3823
|
-
function openHighlightColorPicker() {
|
|
3824
|
-
rememberEditorSelection()
|
|
3825
|
-
highlightColorInputRef.value?.click()
|
|
3826
|
-
}
|
|
3827
|
-
|
|
3828
|
-
function onTextColorChange(event) {
|
|
3829
|
-
const color = event.target.value || DEFAULT_TEXT_COLOR
|
|
3830
|
-
currentTextColor.value = color
|
|
3831
|
-
getEditorCommandWithSelection()?.setColor(color).run()
|
|
3832
|
-
pendingEditorSelection.value = null
|
|
3833
|
-
}
|
|
3834
|
-
|
|
3835
|
-
function onHighlightColorChange(event) {
|
|
3836
|
-
const color = event.target.value || DEFAULT_HIGHLIGHT_COLOR
|
|
3837
|
-
currentHighlightColor.value = color
|
|
3838
|
-
getEditorCommandWithSelection()?.setHighlight({ color }).run()
|
|
3839
|
-
pendingEditorSelection.value = null
|
|
3840
|
-
}
|
|
3841
|
-
|
|
3842
|
-
function clearHighlightColor() {
|
|
3843
|
-
rememberEditorSelection()
|
|
3844
|
-
getEditorCommandWithSelection()?.unsetHighlight().run()
|
|
3845
|
-
currentHighlightColor.value = DEFAULT_HIGHLIGHT_COLOR
|
|
3846
|
-
pendingEditorSelection.value = null
|
|
3847
|
-
}
|
|
3848
|
-
|
|
3849
|
-
function undoEditor() {
|
|
3850
|
-
editor.value?.chain().focus().undo().run()
|
|
3851
|
-
}
|
|
3852
|
-
|
|
3853
|
-
function redoEditor() {
|
|
3854
|
-
editor.value?.chain().focus().redo().run()
|
|
3855
|
-
}
|
|
3856
|
-
|
|
3857
|
-
watch(
|
|
3858
|
-
() => props.renderData,
|
|
3859
|
-
(value) => {
|
|
3860
|
-
editableRenderData.value = createEditableRenderData(value)
|
|
3861
|
-
renderPreview()
|
|
3862
|
-
},
|
|
3863
|
-
{
|
|
3864
|
-
deep: true,
|
|
3865
|
-
immediate: true
|
|
3866
|
-
}
|
|
3867
|
-
)
|
|
3868
|
-
|
|
3869
|
-
watch(
|
|
3870
|
-
() => props.renderType,
|
|
3871
|
-
() => {
|
|
3872
|
-
renderPreview()
|
|
3873
|
-
}
|
|
3874
|
-
)
|
|
3875
|
-
|
|
3876
|
-
defineExpose({
|
|
3877
|
-
exportDocx,
|
|
3878
|
-
getRenderData: () => deepClone(editableRenderData.value),
|
|
3879
|
-
getSavePayload: () => buildSavePayload(),
|
|
3880
|
-
getEditorHtml: () => editor.value?.getHTML() || '',
|
|
3881
|
-
getEditorJson: () => editor.value?.getJSON() || null
|
|
3882
|
-
})
|
|
3883
|
-
|
|
3884
|
-
onBeforeUnmount(() => {
|
|
3885
|
-
renderToken += 1
|
|
3886
|
-
if (outlineScrollRaf) {
|
|
3887
|
-
cancelAnimationFrame(outlineScrollRaf)
|
|
3888
|
-
outlineScrollRaf = 0
|
|
3889
|
-
}
|
|
3890
|
-
clearPreview()
|
|
3891
|
-
editor.value?.destroy()
|
|
3892
|
-
})
|
|
3893
|
-
</script>
|
|
3894
|
-
|
|
3895
|
-
<style scoped lang="less">
|
|
3896
|
-
.view-page {
|
|
3897
|
-
height: 100%;
|
|
3898
|
-
min-height: 0;
|
|
3899
|
-
//position: relative;
|
|
3900
|
-
display: flex;
|
|
3901
|
-
justify-content: stretch;
|
|
3902
|
-
|
|
3903
|
-
.edit-box,
|
|
3904
|
-
.view-box {
|
|
3905
|
-
width: 100%;
|
|
3906
|
-
}
|
|
3907
|
-
|
|
3908
|
-
&.has-outline {
|
|
3909
|
-
justify-content: flex-end;
|
|
3910
|
-
}
|
|
3911
|
-
|
|
3912
|
-
&.has-outline .edit-box,
|
|
3913
|
-
&.has-outline .view-box {
|
|
3914
|
-
width: calc(100% - 250px);
|
|
3915
|
-
}
|
|
3916
|
-
}
|
|
3917
|
-
|
|
3918
|
-
.document-outline {
|
|
3919
|
-
//position: absolute;
|
|
3920
|
-
//top: 18px;
|
|
3921
|
-
//right: 18px;
|
|
3922
|
-
//z-index: 8;
|
|
3923
|
-
width: 240px;
|
|
3924
|
-
max-height: calc(100% - 36px);
|
|
3925
|
-
padding: 14px 12px;
|
|
3926
|
-
overflow: auto;
|
|
3927
|
-
border: 1px solid rgba(19, 191, 166, 0.22);
|
|
3928
|
-
border-radius: 14px;
|
|
3929
|
-
background: rgba(255, 255, 255, 0.92);
|
|
3930
|
-
box-shadow: 0 14px 36px rgba(20, 32, 61, 0.16);
|
|
3931
|
-
backdrop-filter: blur(10px);
|
|
3932
|
-
}
|
|
3933
|
-
|
|
3934
|
-
.outline-title {
|
|
3935
|
-
margin-bottom: 8px;
|
|
3936
|
-
padding: 0 4px 8px;
|
|
3937
|
-
border-bottom: 1px solid #e6edf5;
|
|
3938
|
-
color: #1f2a44;
|
|
3939
|
-
font-size: 14px;
|
|
3940
|
-
font-weight: 700;
|
|
3941
|
-
}
|
|
3942
|
-
|
|
3943
|
-
.outline-item {
|
|
3944
|
-
width: 100%;
|
|
3945
|
-
display: block;
|
|
3946
|
-
margin: 2px 0;
|
|
3947
|
-
padding: 7px 8px;
|
|
3948
|
-
border: none;
|
|
3949
|
-
border-radius: 9px;
|
|
3950
|
-
background: transparent;
|
|
3951
|
-
color: #4f5b72;
|
|
3952
|
-
font-size: 13px;
|
|
3953
|
-
line-height: 1.35;
|
|
3954
|
-
text-align: left;
|
|
3955
|
-
cursor: pointer;
|
|
3956
|
-
transition: all 0.18s ease;
|
|
3957
|
-
}
|
|
3958
|
-
|
|
3959
|
-
.outline-item:hover,
|
|
3960
|
-
.outline-item.is-active {
|
|
3961
|
-
color: #008f7c;
|
|
3962
|
-
background: rgba(19, 191, 166, 0.1);
|
|
3963
|
-
}
|
|
3964
|
-
|
|
3965
|
-
.outline-item.level-2 {
|
|
3966
|
-
padding-left: 20px;
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
|
-
.outline-item.level-3 {
|
|
3970
|
-
padding-left: 34px;
|
|
3971
|
-
font-size: 12px;
|
|
3972
|
-
color: #6f7b91;
|
|
3973
|
-
}
|
|
3974
|
-
|
|
3975
|
-
.outline-text {
|
|
3976
|
-
display: -webkit-box;
|
|
3977
|
-
overflow: hidden;
|
|
3978
|
-
-webkit-line-clamp: 2;
|
|
3979
|
-
-webkit-box-orient: vertical;
|
|
3980
|
-
}
|
|
3981
|
-
|
|
3982
|
-
.view-box,
|
|
3983
|
-
.edit-box {
|
|
3984
|
-
height: 100%;
|
|
3985
|
-
min-height: 0;
|
|
3986
|
-
overflow: auto;
|
|
3987
|
-
padding: 0;
|
|
3988
|
-
background: #858585;
|
|
3989
|
-
}
|
|
3990
|
-
|
|
3991
|
-
.status-box {
|
|
3992
|
-
min-height: 240px;
|
|
3993
|
-
display: flex;
|
|
3994
|
-
align-items: center;
|
|
3995
|
-
justify-content: center;
|
|
3996
|
-
color: #606266;
|
|
3997
|
-
font-size: 14px;
|
|
3998
|
-
background: #fff;
|
|
3999
|
-
border-radius: 8px;
|
|
4000
|
-
height: 100%;
|
|
4001
|
-
}
|
|
4002
|
-
|
|
4003
|
-
.status-box.is-error {
|
|
4004
|
-
color: #f56c6c;
|
|
4005
|
-
}
|
|
4006
|
-
|
|
4007
|
-
.docx-preview-container {
|
|
4008
|
-
min-height: 100%;
|
|
4009
|
-
width: 100%;
|
|
4010
|
-
}
|
|
4011
|
-
|
|
4012
|
-
:deep(.docx-preview-wrapper) {
|
|
4013
|
-
min-height: 100%;
|
|
4014
|
-
width: 100%;
|
|
4015
|
-
box-sizing: border-box;
|
|
4016
|
-
background: #FFFFFF;
|
|
4017
|
-
padding: 0;
|
|
4018
|
-
.docx-preview{
|
|
4019
|
-
width: 100%!important;
|
|
4020
|
-
}
|
|
4021
|
-
}
|
|
4022
|
-
|
|
4023
|
-
.edit-dialog {
|
|
4024
|
-
position: relative;
|
|
4025
|
-
display: flex;
|
|
4026
|
-
flex-direction: column;
|
|
4027
|
-
gap: 12px;
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
|
-
.edit-dialog-tip {
|
|
4031
|
-
color: #4f5b72;
|
|
4032
|
-
font-size: 13px;
|
|
4033
|
-
line-height: 20px;
|
|
4034
|
-
}
|
|
4035
|
-
|
|
4036
|
-
.edit-dialog-path {
|
|
4037
|
-
margin-left: 8px;
|
|
4038
|
-
color: #9098a7;
|
|
4039
|
-
}
|
|
4040
|
-
|
|
4041
|
-
.editor-toolbar {
|
|
4042
|
-
display: flex;
|
|
4043
|
-
flex-wrap: wrap;
|
|
4044
|
-
align-items: center;
|
|
4045
|
-
gap: 8px;
|
|
4046
|
-
padding: 14px;
|
|
4047
|
-
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
|
4048
|
-
border: 1px solid #dbe6f3;
|
|
4049
|
-
border-radius: 14px;
|
|
4050
|
-
box-shadow: 0 10px 30px rgba(31, 42, 68, 0.06);
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
|
-
.toolbar-btn {
|
|
4054
|
-
width: 38px;
|
|
4055
|
-
height: 38px;
|
|
4056
|
-
display: inline-flex;
|
|
4057
|
-
align-items: center;
|
|
4058
|
-
justify-content: center;
|
|
4059
|
-
border: 1px solid #d9e0ec;
|
|
4060
|
-
border-radius: 11px;
|
|
4061
|
-
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
|
4062
|
-
color: #2b3345;
|
|
4063
|
-
cursor: pointer;
|
|
4064
|
-
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 2px 5px rgba(31, 42, 68, 0.04);
|
|
4065
|
-
transition: all 0.16s ease;
|
|
4066
|
-
}
|
|
4067
|
-
|
|
4068
|
-
.toolbar-btn:hover {
|
|
4069
|
-
border-color: #13bfa6;
|
|
4070
|
-
color: #13bfa6;
|
|
4071
|
-
background: linear-gradient(180deg, #ffffff 0%, #effdfa 100%);
|
|
4072
|
-
box-shadow: 0 4px 12px rgba(19, 191, 166, 0.12);
|
|
4073
|
-
}
|
|
4074
|
-
|
|
4075
|
-
.toolbar-btn.is-active {
|
|
4076
|
-
color: #13bfa6;
|
|
4077
|
-
border-color: #13bfa6;
|
|
4078
|
-
background: rgba(19, 191, 166, 0.1);
|
|
4079
|
-
}
|
|
4080
|
-
|
|
4081
|
-
.toolbar-btn:disabled {
|
|
4082
|
-
cursor: not-allowed;
|
|
4083
|
-
opacity: 0.45;
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
|
-
.toolbar-tip-wrap {
|
|
4087
|
-
display: inline-flex;
|
|
4088
|
-
}
|
|
4089
|
-
|
|
4090
|
-
.toolbar-divider {
|
|
4091
|
-
width: 1px;
|
|
4092
|
-
height: 22px;
|
|
4093
|
-
background: #e5e9f2;
|
|
4094
|
-
margin: 0 2px;
|
|
4095
|
-
}
|
|
4096
|
-
|
|
4097
|
-
.toolbar-svg {
|
|
4098
|
-
width: 24px;
|
|
4099
|
-
height: 24px;
|
|
4100
|
-
display: block;
|
|
4101
|
-
color: currentColor;
|
|
4102
|
-
overflow: visible;
|
|
4103
|
-
}
|
|
4104
|
-
|
|
4105
|
-
.heading-svg {
|
|
4106
|
-
width: 26px;
|
|
4107
|
-
height: 26px;
|
|
4108
|
-
}
|
|
4109
|
-
|
|
4110
|
-
.svg-stroke {
|
|
4111
|
-
fill: none;
|
|
4112
|
-
stroke: currentColor;
|
|
4113
|
-
stroke-width: 2;
|
|
4114
|
-
stroke-linecap: square;
|
|
4115
|
-
stroke-linejoin: round;
|
|
4116
|
-
}
|
|
4117
|
-
|
|
4118
|
-
.svg-stroke.thin {
|
|
4119
|
-
stroke-width: 1.25;
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
|
-
.svg-stroke.round {
|
|
4123
|
-
stroke-linecap: round;
|
|
4124
|
-
}
|
|
4125
|
-
|
|
4126
|
-
.svg-stroke.muted {
|
|
4127
|
-
color: #8c96a8;
|
|
4128
|
-
}
|
|
4129
|
-
|
|
4130
|
-
.toolbar-btn:hover .svg-stroke.muted,
|
|
4131
|
-
.toolbar-btn.is-active .svg-stroke.muted {
|
|
4132
|
-
color: currentColor;
|
|
4133
|
-
}
|
|
4134
|
-
|
|
4135
|
-
.svg-stroke.accent {
|
|
4136
|
-
color: #13bfa6;
|
|
4137
|
-
stroke-width: 2.2;
|
|
4138
|
-
}
|
|
4139
|
-
|
|
4140
|
-
.svg-stroke.danger {
|
|
4141
|
-
color: #e47777;
|
|
4142
|
-
stroke-width: 2.2;
|
|
4143
|
-
}
|
|
4144
|
-
|
|
4145
|
-
.svg-soft-fill {
|
|
4146
|
-
fill: currentColor;
|
|
4147
|
-
opacity: 0.14;
|
|
4148
|
-
}
|
|
4149
|
-
|
|
4150
|
-
.svg-color-fill {
|
|
4151
|
-
fill: currentColor;
|
|
4152
|
-
}
|
|
4153
|
-
|
|
4154
|
-
.color-preview-stroke {
|
|
4155
|
-
stroke-width: 2.8;
|
|
4156
|
-
}
|
|
4157
|
-
|
|
4158
|
-
.svg-heading-main,
|
|
4159
|
-
.svg-heading-level,
|
|
4160
|
-
.svg-bold {
|
|
4161
|
-
fill: currentColor;
|
|
4162
|
-
font-family: Georgia, 'Times New Roman', serif;
|
|
4163
|
-
font-weight: 700;
|
|
4164
|
-
}
|
|
4165
|
-
|
|
4166
|
-
.svg-heading-main {
|
|
4167
|
-
font-size: 20px;
|
|
4168
|
-
}
|
|
4169
|
-
|
|
4170
|
-
.svg-heading-level {
|
|
4171
|
-
font-family: Arial, sans-serif;
|
|
4172
|
-
font-size: 10px;
|
|
4173
|
-
font-weight: 800;
|
|
4174
|
-
}
|
|
4175
|
-
|
|
4176
|
-
.svg-bold {
|
|
4177
|
-
font-size: 18px;
|
|
4178
|
-
}
|
|
4179
|
-
|
|
4180
|
-
.color-btn {
|
|
4181
|
-
position: relative;
|
|
4182
|
-
width: 42px;
|
|
4183
|
-
padding: 0;
|
|
4184
|
-
}
|
|
4185
|
-
|
|
4186
|
-
.clear-highlight-btn {
|
|
4187
|
-
width: 38px;
|
|
4188
|
-
}
|
|
4189
|
-
|
|
4190
|
-
.table-trigger-btn {
|
|
4191
|
-
width: 40px;
|
|
4192
|
-
}
|
|
4193
|
-
|
|
4194
|
-
.table-op-btn {
|
|
4195
|
-
width: 44px;
|
|
4196
|
-
overflow: visible;
|
|
4197
|
-
}
|
|
4198
|
-
|
|
4199
|
-
.hidden-color-input {
|
|
4200
|
-
position: absolute;
|
|
4201
|
-
opacity: 0;
|
|
4202
|
-
pointer-events: none;
|
|
4203
|
-
width: 0;
|
|
4204
|
-
height: 0;
|
|
4205
|
-
}
|
|
4206
|
-
|
|
4207
|
-
.table-picker {
|
|
4208
|
-
display: flex;
|
|
4209
|
-
flex-direction: column;
|
|
4210
|
-
gap: 12px;
|
|
4211
|
-
}
|
|
4212
|
-
|
|
4213
|
-
.table-picker-title {
|
|
4214
|
-
color: #1f2a44;
|
|
4215
|
-
font-size: 13px;
|
|
4216
|
-
font-weight: 700;
|
|
4217
|
-
}
|
|
4218
|
-
|
|
4219
|
-
.table-picker-grid {
|
|
4220
|
-
display: grid;
|
|
4221
|
-
grid-template-columns: repeat(10, 1fr);
|
|
4222
|
-
gap: 4px;
|
|
4223
|
-
}
|
|
4224
|
-
|
|
4225
|
-
.table-picker-cell {
|
|
4226
|
-
width: 24px;
|
|
4227
|
-
height: 24px;
|
|
4228
|
-
border: 1px solid #c9d4e5;
|
|
4229
|
-
border-radius: 4px;
|
|
4230
|
-
background: #fff;
|
|
4231
|
-
cursor: pointer;
|
|
4232
|
-
transition: all 0.15s ease;
|
|
4233
|
-
}
|
|
4234
|
-
|
|
4235
|
-
.table-picker-cell.is-active,
|
|
4236
|
-
.table-picker-cell:hover {
|
|
4237
|
-
border-color: #13bfa6;
|
|
4238
|
-
background: rgba(19, 191, 166, 0.14);
|
|
4239
|
-
}
|
|
4240
|
-
|
|
4241
|
-
.table-picker-tip {
|
|
4242
|
-
color: #65708a;
|
|
4243
|
-
font-size: 12px;
|
|
4244
|
-
}
|
|
4245
|
-
|
|
4246
|
-
.table-context-menu {
|
|
4247
|
-
position: absolute;
|
|
4248
|
-
z-index: 20;
|
|
4249
|
-
min-width: 148px;
|
|
4250
|
-
padding: 6px;
|
|
4251
|
-
border: 1px solid #d8dfeb;
|
|
4252
|
-
border-radius: 8px;
|
|
4253
|
-
background: #fff;
|
|
4254
|
-
box-shadow: 0 16px 36px rgba(31, 42, 68, 0.18);
|
|
4255
|
-
}
|
|
4256
|
-
|
|
4257
|
-
.table-context-menu button {
|
|
4258
|
-
width: 100%;
|
|
4259
|
-
height: 30px;
|
|
4260
|
-
display: flex;
|
|
4261
|
-
align-items: center;
|
|
4262
|
-
padding: 0 10px;
|
|
4263
|
-
border: none;
|
|
4264
|
-
border-radius: 6px;
|
|
4265
|
-
background: transparent;
|
|
4266
|
-
color: #2b3345;
|
|
4267
|
-
font-size: 13px;
|
|
4268
|
-
text-align: left;
|
|
4269
|
-
cursor: pointer;
|
|
4270
|
-
}
|
|
4271
|
-
|
|
4272
|
-
.table-context-menu button:hover {
|
|
4273
|
-
background: rgba(19, 191, 166, 0.1);
|
|
4274
|
-
color: #008f7c;
|
|
4275
|
-
}
|
|
4276
|
-
|
|
4277
|
-
.table-context-menu button.is-danger {
|
|
4278
|
-
color: #c45656;
|
|
4279
|
-
}
|
|
4280
|
-
|
|
4281
|
-
.table-context-menu button.is-danger:hover {
|
|
4282
|
-
background: rgba(196, 86, 86, 0.1);
|
|
4283
|
-
}
|
|
4284
|
-
|
|
4285
|
-
.table-context-divider {
|
|
4286
|
-
height: 1px;
|
|
4287
|
-
margin: 5px 4px;
|
|
4288
|
-
background: #edf1f7;
|
|
4289
|
-
}
|
|
4290
|
-
|
|
4291
|
-
.editor-content-shell {
|
|
4292
|
-
min-height: 420px;
|
|
4293
|
-
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
|
|
4294
|
-
border: 1px solid #dbe6f3;
|
|
4295
|
-
border-radius: 18px;
|
|
4296
|
-
overflow: auto;
|
|
4297
|
-
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7), 0 14px 38px rgba(31, 42, 68, 0.08);
|
|
4298
|
-
}
|
|
4299
|
-
|
|
4300
|
-
.dialog-footer {
|
|
4301
|
-
display: flex;
|
|
4302
|
-
justify-content: flex-end;
|
|
4303
|
-
}
|
|
4304
|
-
|
|
4305
|
-
:deep(.docx-wrapper) {
|
|
4306
|
-
min-height: 100%;
|
|
4307
|
-
width: 100%;
|
|
4308
|
-
box-sizing: border-box;
|
|
4309
|
-
padding: 28px 0 48px !important;
|
|
4310
|
-
background: #858585 !important;
|
|
4311
|
-
}
|
|
4312
|
-
|
|
4313
|
-
:deep(.docx-preview) {
|
|
4314
|
-
min-height: 100%;
|
|
4315
|
-
background: #858585;
|
|
4316
|
-
span{
|
|
4317
|
-
line-height: 2em;
|
|
4318
|
-
}
|
|
4319
|
-
}
|
|
4320
|
-
|
|
4321
|
-
:deep(.docx) {
|
|
4322
|
-
margin: 0 auto 24px;
|
|
4323
|
-
box-shadow: 0 16px 40px rgba(20, 32, 61, 0.16);
|
|
4324
|
-
border-radius: 2px;
|
|
4325
|
-
}
|
|
4326
|
-
|
|
4327
|
-
:deep(.docx p),
|
|
4328
|
-
:deep(.docx li),
|
|
4329
|
-
:deep(.docx span),
|
|
4330
|
-
:deep(.docx td),
|
|
4331
|
-
:deep(.docx th) {
|
|
4332
|
-
line-height: 1.6 !important;
|
|
4333
|
-
}
|
|
4334
|
-
|
|
4335
|
-
:deep(.editable-field-anchor) {
|
|
4336
|
-
display: inline;
|
|
4337
|
-
padding: 0;
|
|
4338
|
-
margin: 0;
|
|
4339
|
-
border: none;
|
|
4340
|
-
border-radius: 4px;
|
|
4341
|
-
background: rgba(19, 191, 166, 0.12);
|
|
4342
|
-
cursor: pointer;
|
|
4343
|
-
transition: all 0.2s ease;
|
|
4344
|
-
white-space: pre-wrap;
|
|
4345
|
-
word-break: break-word;
|
|
4346
|
-
box-decoration-break: clone;
|
|
4347
|
-
-webkit-box-decoration-break: clone;
|
|
4348
|
-
font: inherit;
|
|
4349
|
-
font-family: inherit;
|
|
4350
|
-
font-size: inherit;
|
|
4351
|
-
font-weight: inherit;
|
|
4352
|
-
font-style: inherit;
|
|
4353
|
-
line-height: inherit;
|
|
4354
|
-
letter-spacing: inherit;
|
|
4355
|
-
color: inherit;
|
|
4356
|
-
vertical-align: inherit;
|
|
4357
|
-
text-decoration-line: underline;
|
|
4358
|
-
text-decoration-style: dashed;
|
|
4359
|
-
text-decoration-color: rgba(19, 191, 166, 0.85);
|
|
4360
|
-
text-underline-offset: 3px;
|
|
4361
|
-
outline: 1px solid rgba(19, 191, 166, 0.28);
|
|
4362
|
-
box-shadow: 0 0 0 1px rgba(19, 191, 166, 0.08);
|
|
4363
|
-
}
|
|
4364
|
-
|
|
4365
|
-
:deep(.editable-field-anchor:hover) {
|
|
4366
|
-
background: rgba(19, 191, 166, 0.2);
|
|
4367
|
-
text-decoration-color: #13bfa6;
|
|
4368
|
-
outline-color: rgba(19, 191, 166, 0.5);
|
|
4369
|
-
box-shadow: 0 6px 14px rgba(19, 191, 166, 0.12);
|
|
4370
|
-
}
|
|
4371
|
-
|
|
4372
|
-
:deep(.editable-field-anchor.is-placeholder) {
|
|
4373
|
-
color: rgba(79, 91, 114, 0.58);
|
|
4374
|
-
font-style: italic;
|
|
4375
|
-
}
|
|
4376
|
-
|
|
4377
|
-
:deep(.editable-field-block) {
|
|
4378
|
-
display: block;
|
|
4379
|
-
//padding: 6px 8px;
|
|
4380
|
-
margin: 2px 0;
|
|
4381
|
-
border-radius: 8px;
|
|
4382
|
-
background: rgba(19, 191, 166, 0.08);
|
|
4383
|
-
outline: 1px dashed rgba(19, 191, 166, 0.36);
|
|
4384
|
-
cursor: pointer;
|
|
4385
|
-
transition: all 0.2s ease;
|
|
4386
|
-
font: inherit;
|
|
4387
|
-
//font-family: inherit;
|
|
4388
|
-
//font-size: inherit;
|
|
4389
|
-
//font-weight: inherit;
|
|
4390
|
-
//font-style: inherit;
|
|
4391
|
-
line-height: inherit;
|
|
4392
|
-
letter-spacing: inherit;
|
|
4393
|
-
color: inherit;
|
|
4394
|
-
font-size: 16px;
|
|
4395
|
-
padding: 25px!important;
|
|
4396
|
-
}
|
|
4397
|
-
|
|
4398
|
-
:deep(.editable-field-block:hover) {
|
|
4399
|
-
background: rgba(19, 191, 166, 0.14);
|
|
4400
|
-
outline-color: rgba(19, 191, 166, 0.58);
|
|
4401
|
-
box-shadow: 0 8px 18px rgba(19, 191, 166, 0.12);
|
|
4402
|
-
}
|
|
4403
|
-
|
|
4404
|
-
:deep(.editable-field-block.is-placeholder) {
|
|
4405
|
-
min-height: 32px;
|
|
4406
|
-
display: flex;
|
|
4407
|
-
align-items: center;
|
|
4408
|
-
color: rgba(79, 91, 114, 0.58);
|
|
4409
|
-
font-style: italic;
|
|
4410
|
-
}
|
|
4411
|
-
|
|
4412
|
-
:deep(.editable-field-block.is-table-field) {
|
|
4413
|
-
padding: 6px 8px !important;
|
|
4414
|
-
background: rgba(19, 191, 166, 0.04);
|
|
4415
|
-
}
|
|
4416
|
-
|
|
4417
|
-
:deep(.editable-field-block p) {
|
|
4418
|
-
margin: 0 0 0.5em;
|
|
4419
|
-
font: inherit;
|
|
4420
|
-
font-family: inherit;
|
|
4421
|
-
font-size: inherit;
|
|
4422
|
-
font-weight: inherit;
|
|
4423
|
-
font-style: inherit;
|
|
4424
|
-
line-height: inherit;
|
|
4425
|
-
letter-spacing: inherit;
|
|
4426
|
-
color: inherit;
|
|
4427
|
-
}
|
|
4428
|
-
|
|
4429
|
-
:deep(.editable-field-block p:last-child) {
|
|
4430
|
-
margin-bottom: 0;
|
|
4431
|
-
}
|
|
4432
|
-
|
|
4433
|
-
:deep(.ProseMirror) {
|
|
4434
|
-
min-height: 420px;
|
|
4435
|
-
padding: 20px 24px;
|
|
4436
|
-
outline: none;
|
|
4437
|
-
color: #2b3345;
|
|
4438
|
-
font-size: 15px;
|
|
4439
|
-
line-height: 1.9;
|
|
4440
|
-
}
|
|
4441
|
-
|
|
4442
|
-
:deep(.ProseMirror .is-empty.is-editor-empty::before) {
|
|
4443
|
-
content: attr(data-placeholder);
|
|
4444
|
-
height: 0;
|
|
4445
|
-
float: left;
|
|
4446
|
-
color: rgba(79, 91, 114, 0.45);
|
|
4447
|
-
pointer-events: none;
|
|
4448
|
-
}
|
|
4449
|
-
|
|
4450
|
-
:deep(.ProseMirror h1),
|
|
4451
|
-
:deep(.ProseMirror h2),
|
|
4452
|
-
:deep(.ProseMirror h3) {
|
|
4453
|
-
color: #1f2a44;
|
|
4454
|
-
line-height: 1.5;
|
|
4455
|
-
margin: 0 0 16px;
|
|
4456
|
-
}
|
|
4457
|
-
|
|
4458
|
-
:deep(.ProseMirror h1) {
|
|
4459
|
-
font-size: 30px;
|
|
4460
|
-
}
|
|
4461
|
-
|
|
4462
|
-
:deep(.ProseMirror h2) {
|
|
4463
|
-
font-size: 22px;
|
|
4464
|
-
}
|
|
4465
|
-
|
|
4466
|
-
:deep(.ProseMirror h3) {
|
|
4467
|
-
font-size: 18px;
|
|
4468
|
-
}
|
|
4469
|
-
|
|
4470
|
-
:deep(.ProseMirror p) {
|
|
4471
|
-
margin: 0 0 14px;
|
|
4472
|
-
}
|
|
4473
|
-
|
|
4474
|
-
:deep(.ProseMirror table) {
|
|
4475
|
-
width: 100% !important;
|
|
4476
|
-
min-width: 100% !important;
|
|
4477
|
-
max-width: 100% !important;
|
|
4478
|
-
margin: 0 0 16px;
|
|
4479
|
-
border-collapse: collapse;
|
|
4480
|
-
table-layout: fixed;
|
|
4481
|
-
border: 1.2px solid #000;
|
|
4482
|
-
color: #000;
|
|
4483
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY);
|
|
4484
|
-
font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE);
|
|
4485
|
-
line-height: 1.15;
|
|
4486
|
-
}
|
|
4487
|
-
|
|
4488
|
-
:deep(.ProseMirror td),
|
|
4489
|
-
:deep(.ProseMirror th) {
|
|
4490
|
-
position: relative;
|
|
4491
|
-
min-width: 72px;
|
|
4492
|
-
height: 28px;
|
|
4493
|
-
padding: 2px 6px;
|
|
4494
|
-
border: 1.2px solid #000;
|
|
4495
|
-
color: #000;
|
|
4496
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY);
|
|
4497
|
-
font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE);
|
|
4498
|
-
line-height: 1.15;
|
|
4499
|
-
text-align: center;
|
|
4500
|
-
vertical-align: middle;
|
|
4501
|
-
}
|
|
4502
|
-
|
|
4503
|
-
:deep(.ProseMirror th) {
|
|
4504
|
-
background: #a6a6a6;
|
|
4505
|
-
font-size: v-bind(EDITOR_TABLE_HEADER_FONT_SIZE);
|
|
4506
|
-
font-weight: 700;
|
|
4507
|
-
}
|
|
4508
|
-
|
|
4509
|
-
:deep(.ProseMirror td p) {
|
|
4510
|
-
margin: 0;
|
|
4511
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
|
|
4512
|
-
font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE) !important;
|
|
4513
|
-
line-height: 1.15 !important;
|
|
4514
|
-
}
|
|
4515
|
-
|
|
4516
|
-
:deep(.ProseMirror th p) {
|
|
4517
|
-
margin: 0;
|
|
4518
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
|
|
4519
|
-
font-size: v-bind(EDITOR_TABLE_HEADER_FONT_SIZE) !important;
|
|
4520
|
-
line-height: 1.15 !important;
|
|
4521
|
-
}
|
|
4522
|
-
|
|
4523
|
-
:deep(.ProseMirror td *),
|
|
4524
|
-
:deep(.ProseMirror th *) {
|
|
4525
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
|
|
4526
|
-
}
|
|
4527
|
-
|
|
4528
|
-
:deep(.ProseMirror .selectedCell:after) {
|
|
4529
|
-
content: '';
|
|
4530
|
-
position: absolute;
|
|
4531
|
-
inset: 0;
|
|
4532
|
-
background: rgba(19, 191, 166, 0.12);
|
|
4533
|
-
pointer-events: none;
|
|
4534
|
-
}
|
|
4535
|
-
|
|
4536
|
-
:deep(.ProseMirror .tableWrapper) {
|
|
4537
|
-
width: 100% !important;
|
|
4538
|
-
max-width: 100% !important;
|
|
4539
|
-
margin: 0 0 16px;
|
|
4540
|
-
overflow-x: auto;
|
|
4541
|
-
}
|
|
4542
|
-
|
|
4543
|
-
:deep(.editable-field-block table) {
|
|
4544
|
-
width: 100% !important;
|
|
4545
|
-
min-width: 100% !important;
|
|
4546
|
-
max-width: 100% !important;
|
|
4547
|
-
margin: 0 0 12px;
|
|
4548
|
-
border-collapse: collapse;
|
|
4549
|
-
table-layout: fixed;
|
|
4550
|
-
border: 1.2px solid #000;
|
|
4551
|
-
color: #000;
|
|
4552
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY);
|
|
4553
|
-
font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE);
|
|
4554
|
-
line-height: 1.15;
|
|
4555
|
-
}
|
|
4556
|
-
|
|
4557
|
-
:deep(.editable-field-block .tableWrapper) {
|
|
4558
|
-
width: 100% !important;
|
|
4559
|
-
max-width: 100% !important;
|
|
4560
|
-
}
|
|
4561
|
-
|
|
4562
|
-
:deep(.editable-field-block td),
|
|
4563
|
-
:deep(.editable-field-block th) {
|
|
4564
|
-
height: 28px;
|
|
4565
|
-
padding: 2px 6px;
|
|
4566
|
-
border: 1.2px solid #000;
|
|
4567
|
-
color: #000;
|
|
4568
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY);
|
|
4569
|
-
font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE);
|
|
4570
|
-
line-height: 1.15;
|
|
4571
|
-
text-align: center;
|
|
4572
|
-
vertical-align: middle;
|
|
4573
|
-
}
|
|
4574
|
-
|
|
4575
|
-
:deep(.editable-field-block th) {
|
|
4576
|
-
background: #a6a6a6;
|
|
4577
|
-
font-size: v-bind(EDITOR_TABLE_HEADER_FONT_SIZE);
|
|
4578
|
-
font-weight: 700;
|
|
4579
|
-
}
|
|
4580
|
-
|
|
4581
|
-
:deep(.editable-field-block td p) {
|
|
4582
|
-
margin: 0;
|
|
4583
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
|
|
4584
|
-
font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE) !important;
|
|
4585
|
-
line-height: 1.15 !important;
|
|
4586
|
-
}
|
|
4587
|
-
|
|
4588
|
-
:deep(.editable-field-block th p) {
|
|
4589
|
-
margin: 0;
|
|
4590
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
|
|
4591
|
-
font-size: v-bind(EDITOR_TABLE_HEADER_FONT_SIZE) !important;
|
|
4592
|
-
line-height: 1.15 !important;
|
|
4593
|
-
}
|
|
4594
|
-
|
|
4595
|
-
:deep(.editable-field-block td *),
|
|
4596
|
-
:deep(.editable-field-block th *) {
|
|
4597
|
-
font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
|
|
4598
|
-
}
|
|
4599
|
-
|
|
4600
|
-
:deep(.editDialog-dialog){
|
|
4601
|
-
height: 70%;
|
|
4602
|
-
.el-dialog__body{
|
|
4603
|
-
height: calc(100% - 80px);
|
|
4604
|
-
.edit-dialog{
|
|
4605
|
-
height: 100%;
|
|
4606
|
-
.editor-content-shell{
|
|
4607
|
-
height: 100%;
|
|
4608
|
-
overflow: auto;
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4611
|
-
}
|
|
4612
|
-
}
|
|
4613
|
-
</style>
|