vome-core 0.3.42 → 0.3.43

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.
Files changed (73) hide show
  1. package/dist/admin/components/ui/avatar/index.js +1 -1
  2. package/dist/admin/components/ui/checkbox/index.js +1 -1
  3. package/dist/admin/components/ui/dialog/index.js +1 -1
  4. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  5. package/dist/admin/components/ui/input/index.js +1 -1
  6. package/dist/admin/components/ui/label/index.js +1 -1
  7. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  8. package/dist/admin/components/ui/select/index.js +1 -1
  9. package/dist/admin/components/ui/separator/index.js +1 -1
  10. package/dist/admin/components/ui/sheet/index.js +1 -1
  11. package/dist/admin/components/ui/sonner/index.js +1 -1
  12. package/dist/admin/components/ui/table/index.js +1 -1
  13. package/dist/admin/components/ui/tabs/index.js +1 -1
  14. package/dist/admin/components/ui/tooltip/index.js +1 -1
  15. package/dist/admin/crud/auto-fields.js +1 -1
  16. package/dist/admin/crud/components/vm-richtext.vue +110 -923
  17. package/dist/admin/crud/config.js +1 -1
  18. package/dist/admin/crud/confirm.js +1 -1
  19. package/dist/admin/crud/crud-page-request.js +1 -1
  20. package/dist/admin/crud/data-i18n.js +1 -1
  21. package/dist/admin/crud/dict.js +1 -1
  22. package/dist/admin/crud/export-fetch.js +1 -1
  23. package/dist/admin/crud/key.js +1 -1
  24. package/dist/admin/crud/plugins.js +1 -1
  25. package/dist/admin/crud/span.js +1 -1
  26. package/dist/admin/crud/style.js +1 -1
  27. package/dist/admin/crud/validate.js +1 -1
  28. package/dist/admin/directives/perm.js +1 -1
  29. package/dist/admin/hooks/useUpload.js +1 -1
  30. package/dist/admin/lib/browser.js +1 -1
  31. package/dist/admin/lib/cn.js +1 -1
  32. package/dist/admin/lib/dialog-float.js +1 -1
  33. package/dist/admin/lib/export-excel.js +1 -1
  34. package/dist/admin/lib/import-excel.js +1 -1
  35. package/dist/admin/lib/json.js +1 -1
  36. package/dist/admin/lib/menu.js +1 -1
  37. package/dist/admin/lib/micro-locale.js +1 -1
  38. package/dist/admin/lib/micro-theme.js +1 -1
  39. package/dist/admin/lib/shell.js +1 -1
  40. package/dist/admin/lib/theme-color-options.js +1 -1
  41. package/dist/admin/lib/tree.js +1 -1
  42. package/dist/admin/lib/upload.js +1 -1
  43. package/dist/admin/lib/video-frame.js +1 -1
  44. package/dist/admin/router/create.js +1 -1
  45. package/dist/admin/router/guards.js +1 -1
  46. package/dist/ai/index.js +1 -1
  47. package/dist/client/ai-model-registry.js +1 -1
  48. package/dist/client/base.js +1 -1
  49. package/dist/client/configure.js +1 -1
  50. package/dist/client/dict.js +1 -1
  51. package/dist/client/eps-payload.js +1 -1
  52. package/dist/client/eps.js +1 -1
  53. package/dist/client/index.js +1 -1
  54. package/dist/client/locale-change.js +1 -1
  55. package/dist/client/locale-store.js +1 -1
  56. package/dist/client/service.js +1 -1
  57. package/dist/client/vite-admin.js +1 -1
  58. package/dist/client/vite-auto-import.js +1 -1
  59. package/dist/client/vite-micro-proxy.js +1 -1
  60. package/dist/client/vite-plugin-eps.js +1 -1
  61. package/dist/client/vite-resolve.js +1 -1
  62. package/dist/config/index.js +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/server/index.js +1 -1
  65. package/dist/shared/datetime-picker.js +1 -1
  66. package/dist/shared/decimal.js +1 -1
  67. package/dist/shared/excel.js +1 -1
  68. package/dist/shared/index.js +1 -1
  69. package/dist/shared/locale-json.js +1 -1
  70. package/dist/shared/tree.js +1 -1
  71. package/package.json +3 -19
  72. package/dist/admin/crud/components/vm-richtext-extensions.d.ts +0 -28
  73. package/dist/admin/crud/components/vm-richtext-extensions.js +0 -1
@@ -17,153 +17,31 @@
17
17
  v-else
18
18
  class="vm-richtext__shell"
19
19
  >
20
- <div
21
- v-if="editor"
20
+ <Toolbar
22
21
  class="vm-richtext__toolbar"
23
- >
24
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('bold') }" :title="crudLabel('rtBold')" @click="editor.chain().focus().toggleBold().run()"><i class="ri-bold" /></button>
25
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('italic') }" :title="crudLabel('rtItalic')" @click="editor.chain().focus().toggleItalic().run()"><i class="ri-italic" /></button>
26
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('underline') }" :title="crudLabel('rtUnderline')" @click="editor.chain().focus().toggleUnderline().run()"><i class="ri-underline" /></button>
27
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('strike') }" :title="crudLabel('rtStrike')" @click="editor.chain().focus().toggleStrike().run()"><i class="ri-strikethrough" /></button>
28
- <span
29
- class="vm-richtext__btn vm-richtext__color-btn"
30
- :class="{ 'is-active': editor.isActive('highlight'), 'is-disabled': disabled }"
31
- :title="crudLabel('rtBgColor')"
32
- role="button"
33
- @pointerdown="rememberSelection"
34
- >
35
- <i class="ri-mark-pen-line" aria-hidden="true" />
36
- <span class="vm-richtext__color-bar" :style="{ background: highlightColor }" />
37
- <vm-color-picker
38
- class="vm-richtext__color-hit"
39
- size="sm"
40
- swatch-only
41
- :enable-theme="false"
42
- :disabled="disabled"
43
- :model-value="highlightColor"
44
- @update:model-value="applyHighlightColor"
45
- />
46
- </span>
47
- <span
48
- class="vm-richtext__btn vm-richtext__color-btn"
49
- :class="{ 'is-active': Boolean(textColor && textColor !== '#111827'), 'is-disabled': disabled }"
50
- :title="crudLabel('rtTextColor')"
51
- role="button"
52
- @pointerdown="rememberSelection"
53
- >
54
- <i class="ri-font-color" aria-hidden="true" />
55
- <span class="vm-richtext__color-bar" :style="{ background: textColor }" />
56
- <vm-color-picker
57
- class="vm-richtext__color-hit"
58
- size="sm"
59
- swatch-only
60
- :enable-theme="false"
61
- :disabled="disabled"
62
- :model-value="textColor"
63
- @update:model-value="applyTextColor"
64
- />
65
- </span>
66
- <span class="vm-richtext__sep" />
67
- <select
68
- class="vm-richtext__select"
69
- :title="crudLabel('rtFontSize')"
70
- :value="fontSize"
71
- :disabled="disabled"
72
- @change="onFontSizeChange"
73
- >
74
- <option value="">{{ crudLabel('rtFontSizeDefault') }}</option>
75
- <option v-for="s in FONT_SIZES" :key="s" :value="`${s}px`">{{ s }}</option>
76
- </select>
77
- <span class="vm-richtext__sep" />
78
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }" :title="crudLabel('rtAlignLeft')" @click="editor.chain().focus().setTextAlign('left').run()"><i class="ri-align-left" /></button>
79
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }" :title="crudLabel('rtAlignCenter')" @click="editor.chain().focus().setTextAlign('center').run()"><i class="ri-align-center" /></button>
80
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }" :title="crudLabel('rtAlignRight')" @click="editor.chain().focus().setTextAlign('right').run()"><i class="ri-align-right" /></button>
81
- <span class="vm-richtext__sep" />
82
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('bulletList') }" :title="crudLabel('rtBulletList')" @click="editor.chain().focus().toggleBulletList().run()"><i class="ri-list-unordered" /></button>
83
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('orderedList') }" :title="crudLabel('rtOrderedList')" @click="editor.chain().focus().toggleOrderedList().run()"><i class="ri-list-ordered" /></button>
84
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('taskList') }" :title="crudLabel('rtTaskList')" @click="editor.chain().focus().toggleTaskList().run()"><i class="ri-checkbox-line" /></button>
85
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('blockquote') }" :title="crudLabel('rtBlockquote')" @click="editor.chain().focus().toggleBlockquote().run()"><i class="ri-double-quotes-l" /></button>
86
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('codeBlock') }" :title="crudLabel('rtCodeBlock')" @click="editor.chain().focus().toggleCodeBlock().run()"><i class="ri-code-box-line" /></button>
87
- <button type="button" class="vm-richtext__btn" :title="crudLabel('rtHr')" @click="editor.chain().focus().setHorizontalRule().run()"><i class="ri-separator" /></button>
88
- <span class="vm-richtext__sep" />
89
- <button type="button" class="vm-richtext__btn" :class="{ 'is-active': editor.isActive('link') }" :title="crudLabel('rtLink')" @click="toggleLink"><i class="ri-link" /></button>
90
- <button v-if="uploadEnabled" type="button" class="vm-richtext__btn" :title="crudLabel('rtImage')" @click="pickImage"><i class="ri-image-line" /></button>
91
- <button v-if="uploadEnabled" type="button" class="vm-richtext__btn" :title="crudLabel('rtVideo')" @click="pickVideo"><i class="ri-video-line" /></button>
92
- <button
93
- ref="emojiBtnRef"
94
- type="button"
95
- class="vm-richtext__btn"
96
- :class="{ 'is-active': emojiOpen }"
97
- :title="crudLabel('rtEmoji')"
98
- @click="toggleEmoji"
99
- >
100
- <i class="ri-emotion-line" />
101
- </button>
102
- <Teleport to="body">
103
- <div
104
- v-if="emojiOpen"
105
- ref="emojiPanelRef"
106
- class="vm-richtext__emoji-panel vm-dialog-float"
107
- :style="emojiPanelStyle"
108
- @pointerdown.stop
109
- @mousedown.stop
110
- >
111
- <button
112
- v-for="(e, i) in VM_RICHTEXT_EMOJIS"
113
- :key="i"
114
- type="button"
115
- class="vm-richtext__emoji-item"
116
- @click.stop="insertEmoji(e)"
117
- >
118
- {{ e }}
119
- </button>
120
- </div>
121
- </Teleport>
122
- <span class="vm-richtext__sep" />
123
- <button type="button" class="vm-richtext__btn" :title="crudLabel('rtTable')" @click="insertTable"><i class="ri-table-line" /></button>
124
- <button type="button" class="vm-richtext__btn" :title="crudLabel('rtUndo')" :disabled="!editor.can().undo()" @click="editor.chain().focus().undo().run()"><i class="ri-arrow-go-back-line" /></button>
125
- <button type="button" class="vm-richtext__btn" :title="crudLabel('rtRedo')" :disabled="!editor.can().redo()" @click="editor.chain().focus().redo().run()"><i class="ri-arrow-go-forward-line" /></button>
126
- </div>
127
-
128
- <EditorContent class="vm-richtext__content" :editor="editor" />
129
- <vm-context-menu
130
- ref="tableCtxRef"
131
- :items="tableCtxItems"
132
- :z-index="10050"
133
- @select="onTableCtxSelect"
22
+ :editor="editorRef"
23
+ :default-config="toolbarConfig"
24
+ mode="default"
25
+ />
26
+ <Editor
27
+ class="vm-richtext__editor"
28
+ v-model="html"
29
+ :default-config="editorConfig"
30
+ mode="default"
31
+ @onCreated="handleCreated"
134
32
  />
135
33
  </div>
136
34
  </div>
137
35
  </template>
138
36
 
139
37
  <script setup lang="ts">
38
+ import '@wangeditor/editor/dist/css/style.css'
140
39
  import { crudLabel } from '../style'
141
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
40
+ import { computed, onBeforeUnmount, shallowRef, watch } from 'vue'
142
41
  import DOMPurify from 'dompurify'
143
- import { EditorContent, useEditor } from '@tiptap/vue-3'
144
- import StarterKit from '@tiptap/starter-kit'
145
- import Image from '@tiptap/extension-image'
146
- import Link from '@tiptap/extension-link'
147
- import Placeholder from '@tiptap/extension-placeholder'
148
- import Underline from '@tiptap/extension-underline'
149
- import TextAlign from '@tiptap/extension-text-align'
150
- import Highlight from '@tiptap/extension-highlight'
151
- import TextStyle from '@tiptap/extension-text-style'
152
- import Color from '@tiptap/extension-color'
153
- import TaskList from '@tiptap/extension-task-list'
154
- import TaskItem from '@tiptap/extension-task-item'
155
- import Table from '@tiptap/extension-table'
156
- import TableRow from '@tiptap/extension-table-row'
157
- import TableHeader from '@tiptap/extension-table-header'
158
- import TableCell from '@tiptap/extension-table-cell'
42
+ import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
43
+ import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
159
44
  import { useUpload } from '../../hooks/useUpload'
160
- import { FONT_SIZES, FontSize, VM_RICHTEXT_EMOJIS, VmRichtextVideo } from './vm-richtext-extensions'
161
- import VmColorPicker from './vm-color-picker.vue'
162
- import VmContextMenu from './vm-context-menu.vue'
163
- import { CellSelection, cellAround } from '@tiptap/pm/tables'
164
- import type { Editor } from '@tiptap/core'
165
- import type { EditorView } from '@tiptap/pm/view'
166
- import type { CtxMenuItem } from '../../../../typings/admin/comm/crud'
167
45
 
168
46
  defineOptions({ name: 'vm-richtext' })
169
47
 
@@ -197,218 +75,8 @@ const props = withDefaults(
197
75
  const emit = defineEmits<{ 'update:modelValue': [v: string] }>()
198
76
 
199
77
  const { toUpload } = useUpload()
78
+ const editorRef = shallowRef<IDomEditor>()
200
79
  const syncing = shallowRef(false)
201
- const uploading = shallowRef(false)
202
- const emojiOpen = shallowRef(false)
203
- const emojiBtnRef = shallowRef<HTMLElement>()
204
- const emojiPanelRef = shallowRef<HTMLElement>()
205
- const emojiPanelStyle = shallowRef<Record<string, string>>({})
206
- const textColor = ref('#111827')
207
- const highlightColor = ref('#fef08a')
208
- /** 当前选区字号;空串 = 默认(编辑区基线 14px) */
209
- const fontSize = ref('')
210
- /** 点开取色器前保存选区,避免焦点离开编辑器后丢标记范围 */
211
- let savedSelection: { from: number; to: number } | null = null
212
-
213
- const tableCtxRef = ref<{ open: (ev: MouseEvent) => void; close: () => void } | null>(null)
214
- const tableCtxItems = ref<CtxMenuItem[]>([])
215
- /** 右键按下时缓存多选,防止 mousedown 先把 CellSelection 清掉 */
216
- let pendingCellSel: { anchor: number; head: number } | null = null
217
-
218
- type TableCtxKey =
219
- | 'addColumnBefore'
220
- | 'addColumnAfter'
221
- | 'addRowBefore'
222
- | 'addRowAfter'
223
- | 'deleteColumn'
224
- | 'deleteRow'
225
- | 'mergeCells'
226
- | 'splitCell'
227
-
228
- function cellPosInSelection(sel: CellSelection, cellPos: number) {
229
- let hit = false
230
- sel.forEachCell((_node, pos) => {
231
- if (pos === cellPos) hit = true
232
- })
233
- return hit
234
- }
235
-
236
- function restorePendingCellSelection(view: EditorView) {
237
- if (!pendingCellSel) return false
238
- try {
239
- const sel = CellSelection.create(
240
- view.state.doc,
241
- pendingCellSel.anchor,
242
- pendingCellSel.head,
243
- )
244
- view.dispatch(view.state.tr.setSelection(sel))
245
- return true
246
- } catch {
247
- return false
248
- }
249
- }
250
-
251
- /**
252
- * 右键 mousedown:表格插件会先清掉多选。
253
- * 若点在已有 CellSelection 内 → preventDefault 并记下选区,供 contextmenu 恢复。
254
- */
255
- function onTableMouseDown(view: EditorView, event: MouseEvent) {
256
- if (event.button !== 2) {
257
- pendingCellSel = null
258
- return false
259
- }
260
- const target = event.target as HTMLElement | null
261
- if (!target?.closest?.('td, th')) {
262
- pendingCellSel = null
263
- return false
264
- }
265
-
266
- const { selection } = view.state
267
- if (!(selection instanceof CellSelection)) {
268
- pendingCellSel = null
269
- return false
270
- }
271
-
272
- pendingCellSel = {
273
- anchor: selection.$anchorCell.pos,
274
- head: selection.$headCell.pos,
275
- }
276
-
277
- const coords = view.posAtCoords({ left: event.clientX, top: event.clientY })
278
- if (!coords) return false
279
- const $cell = cellAround(view.state.doc.resolve(coords.pos))
280
- if (!$cell) return false
281
-
282
- if (cellPosInSelection(selection, $cell.pos)) {
283
- event.preventDefault()
284
- return true
285
- }
286
- return false
287
- }
288
-
289
- /** 右键落点:已在多选内则保留;否则选中该单元格作为参照 */
290
- function selectTableCellFromEvent(ed: Editor, event: MouseEvent) {
291
- const view = ed.view
292
- const coords = view.posAtCoords({ left: event.clientX, top: event.clientY })
293
- if (!coords) return false
294
- const $cell = cellAround(view.state.doc.resolve(coords.pos))
295
- if (!$cell) return false
296
-
297
- const { selection } = view.state
298
- if (selection instanceof CellSelection && cellPosInSelection(selection, $cell.pos)) {
299
- return true
300
- }
301
-
302
- // mousedown 已清多选时,用缓存恢复(且右键仍落在原多选内)
303
- if (pendingCellSel) {
304
- const probe = CellSelection.create(
305
- view.state.doc,
306
- pendingCellSel.anchor,
307
- pendingCellSel.head,
308
- )
309
- if (cellPosInSelection(probe, $cell.pos)) {
310
- return restorePendingCellSelection(view)
311
- }
312
- }
313
-
314
- const tr = view.state.tr.setSelection(CellSelection.create(view.state.doc, $cell.pos))
315
- view.dispatch(tr)
316
- return true
317
- }
318
-
319
- function buildTableCtxItems(ed: Editor): CtxMenuItem[] {
320
- const can = ed.can()
321
- return [
322
- {
323
- key: 'addColumnBefore',
324
- label: crudLabel('rtTableColBefore'),
325
- disabled: !can.addColumnBefore(),
326
- },
327
- {
328
- key: 'addColumnAfter',
329
- label: crudLabel('rtTableColAfter'),
330
- disabled: !can.addColumnAfter(),
331
- },
332
- {
333
- key: 'addRowBefore',
334
- label: crudLabel('rtTableRowBefore'),
335
- disabled: !can.addRowBefore(),
336
- },
337
- {
338
- key: 'addRowAfter',
339
- label: crudLabel('rtTableRowAfter'),
340
- disabled: !can.addRowAfter(),
341
- },
342
- {
343
- key: 'deleteColumn',
344
- label: crudLabel('rtTableDeleteCol'),
345
- disabled: !can.deleteColumn(),
346
- },
347
- {
348
- key: 'deleteRow',
349
- label: crudLabel('rtTableDeleteRow'),
350
- disabled: !can.deleteRow(),
351
- },
352
- {
353
- key: 'mergeCells',
354
- label: crudLabel('rtTableMerge'),
355
- disabled: !can.mergeCells(),
356
- },
357
- {
358
- key: 'splitCell',
359
- label: crudLabel('rtTableSplit'),
360
- disabled: !can.splitCell(),
361
- },
362
- ]
363
- }
364
-
365
- function onTableContextMenu(view: EditorView, event: MouseEvent) {
366
- const ed = editor.value
367
- if (!ed || ed.isDestroyed || props.disabled) return false
368
- const target = event.target as HTMLElement | null
369
- if (!target?.closest?.('td, th')) return false
370
- if (!selectTableCellFromEvent(ed, event)) return false
371
- event.preventDefault()
372
- tableCtxItems.value = buildTableCtxItems(ed)
373
- tableCtxRef.value?.open(event)
374
- pendingCellSel = null
375
- return true
376
- }
377
-
378
- function onTableCtxSelect(key: string) {
379
- const ed = editor.value
380
- if (!ed) return
381
- const k = key as TableCtxKey
382
- const chain = ed.chain().focus()
383
- switch (k) {
384
- case 'addColumnBefore':
385
- chain.addColumnBefore().run()
386
- break
387
- case 'addColumnAfter':
388
- chain.addColumnAfter().run()
389
- break
390
- case 'addRowBefore':
391
- chain.addRowBefore().run()
392
- break
393
- case 'addRowAfter':
394
- chain.addRowAfter().run()
395
- break
396
- case 'deleteColumn':
397
- chain.deleteColumn().run()
398
- break
399
- case 'deleteRow':
400
- chain.deleteRow().run()
401
- break
402
- case 'mergeCells':
403
- chain.mergeCells().run()
404
- break
405
- case 'splitCell':
406
- chain.splitCell().run()
407
- break
408
- default:
409
- break
410
- }
411
- }
412
80
 
413
81
  const readOnlyMode = computed(() => props.readonly)
414
82
  const uploadEnabled = computed(() => Boolean(props.uploadImage) || props.autoUpload !== false)
@@ -436,6 +104,8 @@ function normalizeHtml(html: string) {
436
104
 
437
105
  const safeHtml = computed(() => sanitize(String(props.modelValue || '')))
438
106
 
107
+ const html = shallowRef(normalizeHtml(String(props.modelValue || '')) || '<p><br></p>')
108
+
439
109
  async function resolveUpload(file: File): Promise<string> {
440
110
  if (props.uploadImage) return props.uploadImage(file)
441
111
  if (props.autoUpload === false) throw new Error(crudLabel('noUploadImage'))
@@ -443,321 +113,67 @@ async function resolveUpload(file: File): Promise<string> {
443
113
  return res.url
444
114
  }
445
115
 
446
- async function insertImage(file: File) {
447
- if (!editor.value || uploading.value) return
448
- uploading.value = true
449
- try {
450
- const url = await resolveUpload(file)
451
- editor.value.chain().focus().setImage({ src: url, alt: file.name || 'image' }).run()
452
- } finally {
453
- uploading.value = false
454
- }
455
- }
456
-
457
- async function insertVideo(file: File) {
458
- if (!editor.value || uploading.value) return
459
- uploading.value = true
460
- try {
461
- const url = await resolveUpload(file)
462
- editor.value.chain().focus().setVideo({ src: url }).run()
463
- } finally {
464
- uploading.value = false
465
- }
466
- }
467
-
468
- function pickFile(accept: string, handler: (file: File) => void) {
469
- const input = document.createElement('input')
470
- input.type = 'file'
471
- input.accept = accept
472
- input.onchange = () => {
473
- const file = input.files?.[0]
474
- if (file) handler(file)
475
- }
476
- input.click()
477
- }
478
-
479
- function pickImage() {
480
- pickFile('image/*', (file) => void insertImage(file))
481
- }
482
-
483
- function pickVideo() {
484
- pickFile('video/*', (file) => void insertVideo(file))
485
- }
486
-
487
- function placeEmojiPanel() {
488
- const btn = emojiBtnRef.value
489
- if (!btn) return
490
- const rect = btn.getBoundingClientRect()
491
- // 10×28 + 9×2 gap + 16 padding
492
- const panelW = 314
493
- const panelH = 280
494
- const gap = 6
495
- const pad = 8
496
- // 右缘对齐按钮(往左展开,避免溢出大弹窗右侧)
497
- let left = rect.right - panelW
498
- let top = rect.bottom + gap
499
-
500
- const host =
501
- (btn.closest('[data-slot="dialog-content"]') as HTMLElement | null) ||
502
- (btn.closest('.vm-dialog-shell-crud') as HTMLElement | null)
503
- const bound = host?.getBoundingClientRect() ?? {
504
- left: pad,
505
- right: window.innerWidth - pad,
506
- top: pad,
507
- bottom: window.innerHeight - pad,
508
- }
509
-
510
- left = Math.min(left, bound.right - panelW - pad)
511
- left = Math.max(left, bound.left + pad)
512
- if (top + panelH > bound.bottom - pad) {
513
- top = Math.max(bound.top + pad, rect.top - panelH - gap)
514
- }
515
-
516
- emojiPanelStyle.value = {
517
- position: 'fixed',
518
- left: `${Math.round(left)}px`,
519
- top: `${Math.round(top)}px`,
520
- width: `${panelW}px`,
521
- zIndex: '10050',
522
- }
523
- }
524
-
525
- async function toggleEmoji() {
526
- emojiOpen.value = !emojiOpen.value
527
- if (emojiOpen.value) {
528
- await nextTick()
529
- placeEmojiPanel()
530
- }
531
- }
532
-
533
- function insertEmoji(emoji: string) {
534
- editor.value?.chain().focus().insertContent(emoji).run()
535
- emojiOpen.value = false
536
- }
537
-
538
- function rememberSelection() {
539
- const ed = editor.value
540
- if (!ed) return
541
- const { from, to } = ed.state.selection
542
- savedSelection = { from, to }
543
- }
544
-
545
- function syncColorModels() {
546
- const ed = editor.value
547
- if (!ed) return
548
- const fg = ed.getAttributes('textStyle').color as string | undefined
549
- textColor.value = fg?.trim() || '#111827'
550
- const bg = ed.getAttributes('highlight').color as string | undefined
551
- highlightColor.value = bg?.trim() || '#fef08a'
552
- const size = ed.getAttributes('textStyle').fontSize as string | undefined
553
- fontSize.value = size?.trim() || ''
554
- }
555
-
556
- function onFontSizeChange(e: Event) {
557
- const ed = editor.value
558
- if (!ed) return
559
- const v = String((e.target as HTMLSelectElement).value || '').trim()
560
- fontSize.value = v
561
- if (!v) {
562
- ed.chain().focus().unsetFontSize().run()
563
- return
564
- }
565
- ed.chain().focus().setFontSize(v).run()
566
- }
567
-
568
- function withSavedSelection(run: (chain: {
569
- unsetColor: () => { run: () => boolean }
570
- setColor: (c: string) => { run: () => boolean }
571
- unsetHighlight: () => { run: () => boolean }
572
- setHighlight: (attrs: { color: string }) => { run: () => boolean }
573
- }) => void) {
574
- const ed = editor.value
575
- if (!ed) return
576
- const chain = ed.chain().focus()
577
- if (savedSelection) {
578
- chain.setTextSelection(savedSelection)
579
- }
580
- run(chain as never)
581
- savedSelection = {
582
- from: ed.state.selection.from,
583
- to: ed.state.selection.to,
584
- }
585
- syncColorModels()
586
- }
587
-
588
- function applyTextColor(v: string) {
589
- const color = String(v || '').trim()
590
- withSavedSelection((chain) => {
591
- if (!color) {
592
- chain.unsetColor().run()
593
- return
594
- }
595
- chain.setColor(color).run()
596
- })
597
- }
598
-
599
- function applyHighlightColor(v: string) {
600
- const color = String(v || '').trim()
601
- withSavedSelection((chain) => {
602
- if (!color) {
603
- chain.unsetHighlight().run()
604
- return
605
- }
606
- chain.setHighlight({ color }).run()
607
- })
608
- }
609
-
610
- function insertTable() {
611
- editor.value
612
- ?.chain()
613
- .focus()
614
- .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
615
- .run()
616
- }
617
-
618
- function toggleLink() {
619
- const ed = editor.value
620
- if (!ed) return
621
- const prev = ed.getAttributes('link').href as string | undefined
622
- if (ed.isActive('link')) {
623
- ed.chain().focus().unsetLink().run()
624
- return
625
- }
626
- const url = window.prompt(crudLabel('linkUrlPrompt'), prev || 'https://')
627
- if (url == null) return
628
- const href = url.trim()
629
- if (!href) {
630
- ed.chain().focus().unsetLink().run()
631
- return
632
- }
633
- ed.chain().focus().extendMarkRange('link').setLink({ href, target: '_blank', rel: 'noopener noreferrer' }).run()
634
- }
635
-
636
- function handleMediaPaste(event: ClipboardEvent) {
637
- if (!uploadEnabled.value) return false
638
- const items = Array.from(event.clipboardData?.items || [])
639
- const imageItem = items.find((item) => item.type.startsWith('image/'))
640
- if (imageItem) {
641
- const file = imageItem.getAsFile()
642
- if (!file) return false
643
- event.preventDefault()
644
- void insertImage(file)
645
- return true
646
- }
647
- const videoItem = items.find((item) => item.type.startsWith('video/'))
648
- if (videoItem) {
649
- const file = videoItem.getAsFile()
650
- if (!file) return false
651
- event.preventDefault()
652
- void insertVideo(file)
653
- return true
654
- }
655
- return false
656
- }
657
-
658
- function handleMediaDrop(event: DragEvent, moved: boolean) {
659
- if (moved || !uploadEnabled.value) return false
660
- const files = Array.from(event.dataTransfer?.files || [])
661
- const image = files.find((f) => f.type.startsWith('image/'))
662
- if (image) {
663
- event.preventDefault()
664
- void insertImage(image)
665
- return true
666
- }
667
- const video = files.find((f) => f.type.startsWith('video/'))
668
- if (video) {
669
- event.preventDefault()
670
- void insertVideo(video)
671
- return true
672
- }
673
- return false
674
- }
116
+ const toolbarConfig = computed<Partial<IToolbarConfig>>(() => ({
117
+ excludeKeys: uploadEnabled.value ? [] : ['uploadImage', 'uploadVideo'],
118
+ }))
675
119
 
676
- const editor = useEditor({
677
- content: normalizeHtml(String(props.modelValue || '')),
678
- editable: !props.disabled,
679
- extensions: [
680
- StarterKit.configure({ heading: false }),
681
- Underline,
682
- TextStyle,
683
- Color,
684
- FontSize,
685
- Highlight.configure({ multicolor: true }),
686
- TextAlign.configure({ types: ['paragraph'] }),
687
- TaskList,
688
- TaskItem.configure({ nested: true }),
689
- Link.configure({
690
- openOnClick: false,
691
- HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' },
692
- }),
693
- Image.configure({ allowBase64: false }),
694
- VmRichtextVideo,
695
- Table.configure({ resizable: true }),
696
- TableRow,
697
- TableHeader,
698
- TableCell,
699
- Placeholder.configure({
700
- placeholder: props.placeholder ?? crudLabel('richtextPlaceholder'),
701
- }),
702
- ],
703
- editorProps: {
704
- attributes: { class: 'vm-richtext__prose' },
705
- handlePaste: (_view, event) => handleMediaPaste(event),
706
- handleDrop: (_view, event, _slice, moved) => handleMediaDrop(event, moved),
707
- handleDOMEvents: {
708
- mousedown: (view, event) => onTableMouseDown(view, event),
709
- contextmenu: (view, event) => onTableContextMenu(view, event),
120
+ const editorConfig = computed<Partial<IEditorConfig>>(() => ({
121
+ placeholder: props.placeholder ?? crudLabel('richtextPlaceholder'),
122
+ MENU_CONF: {
123
+ uploadImage: {
124
+ async customUpload(file: File, insertFn: (url: string, alt: string, href: string) => void) {
125
+ const url = await resolveUpload(file)
126
+ insertFn(url, file.name || 'image', url)
127
+ },
128
+ },
129
+ uploadVideo: {
130
+ async customUpload(file: File, insertFn: (url: string, poster: string) => void) {
131
+ const url = await resolveUpload(file)
132
+ insertFn(url, '')
133
+ },
710
134
  },
711
135
  },
712
- onUpdate: ({ editor: ed }) => {
713
- if (syncing.value) return
714
- emit('update:modelValue', normalizeHtml(ed.getHTML()))
715
- },
716
- onSelectionUpdate: () => {
717
- syncColorModels()
718
- },
719
- onTransaction: () => {
720
- syncColorModels()
721
- },
722
- })
136
+ }))
723
137
 
724
- watch(() => props.modelValue, (next) => {
725
- const ed = editor.value
726
- if (!ed || readOnlyMode.value) return
727
- const html = normalizeHtml(String(next || ''))
728
- const current = normalizeHtml(ed.getHTML())
729
- if (current === html) return
730
- syncing.value = true
731
- ed.commands.setContent(html || '<p></p>', false)
732
- syncing.value = false
733
- })
138
+ function handleCreated(editor: IDomEditor) {
139
+ editorRef.value = editor
140
+ if (props.disabled) editor.disable()
141
+ else editor.enable()
142
+ }
734
143
 
735
- watch(() => props.disabled, (v) => {
736
- editor.value?.setEditable(!v)
144
+ watch(html, (v) => {
145
+ if (syncing.value || readOnlyMode.value) return
146
+ emit('update:modelValue', normalizeHtml(String(v || '')))
737
147
  })
738
148
 
739
- function onDocPointer(e: Event) {
740
- if (!emojiOpen.value) return
741
- const t = e.target as Node
742
- if (emojiBtnRef.value?.contains(t)) return
743
- if (emojiPanelRef.value?.contains(t)) return
744
- emojiOpen.value = false
745
- }
149
+ watch(
150
+ () => props.modelValue,
151
+ (next) => {
152
+ if (readOnlyMode.value) return
153
+ const n = normalizeHtml(String(next || ''))
154
+ const cur = normalizeHtml(String(html.value || ''))
155
+ if (n === cur) return
156
+ syncing.value = true
157
+ html.value = n || '<p><br></p>'
158
+ syncing.value = false
159
+ },
160
+ )
746
161
 
747
- function onWinReposition() {
748
- if (emojiOpen.value) placeEmojiPanel()
749
- }
162
+ watch(
163
+ () => props.disabled,
164
+ (v) => {
165
+ const ed = editorRef.value
166
+ if (!ed) return
167
+ if (v) ed.disable()
168
+ else ed.enable()
169
+ },
170
+ )
750
171
 
751
- onMounted(() => {
752
- document.addEventListener('pointerdown', onDocPointer, true)
753
- window.addEventListener('resize', onWinReposition)
754
- window.addEventListener('scroll', onWinReposition, true)
755
- })
756
172
  onBeforeUnmount(() => {
757
- document.removeEventListener('pointerdown', onDocPointer, true)
758
- window.removeEventListener('resize', onWinReposition)
759
- window.removeEventListener('scroll', onWinReposition, true)
760
- editor.value?.destroy()
173
+ const ed = editorRef.value
174
+ if (!ed) return
175
+ ed.destroy()
176
+ editorRef.value = undefined
761
177
  })
762
178
  </script>
763
179
 
@@ -765,7 +181,6 @@ onBeforeUnmount(() => {
765
181
  .vm-richtext {
766
182
  width: 100%;
767
183
  min-width: 0;
768
- box-sizing: border-box;
769
184
  }
770
185
 
771
186
  .vm-richtext.is-disabled {
@@ -776,136 +191,21 @@ onBeforeUnmount(() => {
776
191
  .vm-richtext__shell {
777
192
  display: flex;
778
193
  flex-direction: column;
779
- width: 100%;
780
- min-width: 0;
781
- border: 1px solid var(--input);
782
- border-radius: var(--vm-control-radius);
783
- background: var(--card);
784
194
  overflow: hidden;
785
- }
786
-
787
- .vm-richtext__toolbar {
788
- display: flex;
789
- flex-wrap: wrap;
790
- align-items: center;
791
- gap: 2px;
792
- padding: 6px 8px;
793
- border-bottom: 1px solid var(--border);
794
- background: var(--muted);
795
- }
796
-
797
- .vm-richtext__btn {
798
- display: inline-flex;
799
- align-items: center;
800
- justify-content: center;
801
- min-width: 28px;
802
- height: 28px;
803
- padding: 0 6px;
804
- border: none;
805
- border-radius: 6px;
806
- background: transparent;
807
- color: var(--foreground);
808
- font-size: 12px;
809
- font-weight: 600;
810
- line-height: 1;
811
- cursor: pointer;
812
- transition: background 0.15s ease, color 0.15s ease;
813
-
814
- i { font-size: 15px; line-height: 1; }
815
-
816
- &:hover:not(:disabled) {
817
- background: color-mix(in srgb, var(--brand) 10%, transparent);
818
- color: var(--brand);
819
- }
820
-
821
- &.is-active {
822
- background: color-mix(in srgb, var(--brand) 16%, transparent);
823
- color: var(--brand);
824
- }
825
-
826
- &:disabled { opacity: 0.35; cursor: not-allowed; }
827
- }
828
-
829
- .vm-richtext__select {
830
- height: 28px;
831
- min-width: 64px;
832
- padding: 0 6px;
833
195
  border: 1px solid var(--border);
834
- border-radius: 6px;
196
+ border-radius: var(--radius, 8px);
835
197
  background: var(--card);
836
- color: var(--foreground);
837
- font-size: 12px;
838
- line-height: 1;
839
- cursor: pointer;
840
-
841
- &:disabled { opacity: 0.35; cursor: not-allowed; }
842
- }
843
-
844
- /** 保留原图标 + 底部色条;透明覆盖层承接 vm-color-picker 点击 */
845
- .vm-richtext__color-btn {
846
- position: relative;
847
- display: inline-flex;
848
- flex-direction: column;
849
- align-items: center;
850
- justify-content: center;
851
- gap: 1px;
852
- padding: 0 5px 3px;
853
-
854
- &.is-disabled {
855
- opacity: 0.35;
856
- pointer-events: none;
857
- }
858
-
859
- i {
860
- position: relative;
861
- z-index: 1;
862
- pointer-events: none;
863
- }
864
- }
865
-
866
- .vm-richtext__color-bar {
867
- position: relative;
868
- z-index: 1;
869
- display: block;
870
- width: 14px;
871
- height: 3px;
872
- border-radius: 1px;
873
- pointer-events: none;
874
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 70%, transparent);
875
198
  }
876
199
 
877
- .vm-richtext__color-hit {
878
- position: absolute !important;
879
- inset: 0;
880
- z-index: 2;
881
- width: auto !important;
882
- opacity: 0;
883
-
884
- :deep(.vm-color-picker__swatch) {
885
- width: 100%;
886
- height: 100%;
887
- border: none;
888
- border-radius: 6px;
889
- background: transparent;
890
- }
891
-
892
- :deep(.vm-color-picker__swatch-fill) {
893
- display: none;
894
- }
895
- }
896
-
897
- .vm-richtext__sep {
898
- width: 1px;
899
- height: 18px;
900
- margin: 0 4px;
901
- background: var(--border);
200
+ .vm-richtext__toolbar {
201
+ border-bottom: 1px solid var(--border) !important;
202
+ background: var(--muted) !important;
902
203
  }
903
204
 
904
- .vm-richtext__content {
905
- height: var(--vm-richtext-height, 320px) !important;
906
- min-height: var(--vm-richtext-height, 320px) !important;
205
+ .vm-richtext__editor {
907
206
  flex-shrink: 0;
908
- overflow-y: auto;
207
+ height: var(--vm-richtext-height, 320px) !important;
208
+ overflow-y: hidden;
909
209
  background: var(--card);
910
210
  }
911
211
 
@@ -918,7 +218,6 @@ onBeforeUnmount(() => {
918
218
 
919
219
  :deep(p) { margin: 0.5em 0; }
920
220
  :deep(strong), :deep(b) { font-weight: 800; }
921
- /* Tailwind preflight 会清掉 list-style,只读展示也要补回 */
922
221
  :deep(ul) {
923
222
  margin: 0.5em 0;
924
223
  padding-left: 1.5em;
@@ -936,28 +235,21 @@ onBeforeUnmount(() => {
936
235
  :deep(ol ol) { list-style-type: lower-alpha; }
937
236
  :deep(ol ol ol) { list-style-type: lower-roman; }
938
237
  :deep(li) { display: list-item; }
939
- :deep(ul[data-type='taskList']) {
940
- list-style: none;
941
- padding-left: 0.25em;
942
- }
943
- :deep(ul[data-type='taskList'] li) {
944
- display: flex;
945
- gap: 8px;
946
- align-items: center;
947
- }
948
- :deep(ul[data-type='taskList'] li > label) {
949
- display: flex;
950
- align-items: center;
951
- flex-shrink: 0;
952
- margin: 0;
953
- }
954
- :deep(ul[data-type='taskList'] li > div > p) { margin: 0; }
955
238
  :deep(a) { color: var(--brand); text-decoration: underline; }
956
239
  :deep(img), :deep(video) { max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
957
240
  :deep(table) { width: 100%; border-collapse: collapse; margin: 8px 0; }
958
241
  :deep(th), :deep(td) { border: 1px solid var(--border); padding: 6px 8px; }
959
- :deep(blockquote) { margin: 0.5em 0; padding-left: 12px; border-left: 3px solid var(--border); color: var(--muted-foreground); }
960
- :deep(h1), :deep(h2), :deep(h3), :deep(h4) { margin: 0.65em 0 0.35em; font-weight: 650; line-height: 1.35; }
242
+ :deep(blockquote) {
243
+ margin: 0.5em 0;
244
+ padding-left: 12px;
245
+ border-left: 3px solid var(--border);
246
+ color: var(--muted-foreground);
247
+ }
248
+ :deep(h1), :deep(h2), :deep(h3), :deep(h4) {
249
+ margin: 0.65em 0 0.35em;
250
+ font-weight: 650;
251
+ line-height: 1.35;
252
+ }
961
253
  }
962
254
 
963
255
  .vm-richtext.is-readonly .vm-richtext__html {
@@ -965,145 +257,40 @@ onBeforeUnmount(() => {
965
257
  border: none;
966
258
  background: transparent;
967
259
  }
968
- </style>
969
260
 
970
- <style lang="scss">
971
- /* Teleport 到 body:Dialog modal 会给 body pointer-events:none,须 vm-dialog-float 恢复 */
972
- .vm-richtext__emoji-panel {
973
- box-sizing: border-box;
974
- display: grid;
975
- grid-template-columns: repeat(10, 28px);
976
- gap: 2px;
977
- width: 314px;
978
- max-height: 280px;
979
- overflow-x: hidden;
980
- overflow-y: auto;
981
- overscroll-behavior: contain;
982
- padding: 8px;
983
- border: 1px solid var(--border);
984
- border-radius: 10px;
985
- background: var(--card);
986
- box-shadow: var(--shadow-soft, 0 8px 24px rgba(0, 0, 0, 0.12));
987
- scrollbar-width: thin;
988
- pointer-events: auto;
261
+ :deep(.w-e-toolbar) {
262
+ border: none !important;
263
+ background: transparent !important;
989
264
  }
990
265
 
991
- .vm-richtext__emoji-item {
992
- display: flex;
993
- align-items: center;
994
- justify-content: center;
995
- width: 28px;
996
- height: 28px;
997
- border: none;
998
- border-radius: 6px;
999
- background: transparent;
1000
- font-size: 18px;
1001
- line-height: 1;
1002
- cursor: pointer;
266
+ :deep(.w-e-text-container) {
267
+ border: none !important;
268
+ background: var(--card) !important;
269
+ }
1003
270
 
1004
- &:hover {
1005
- background: var(--muted);
1006
- }
271
+ :deep(.w-e-scroll) {
272
+ height: var(--vm-richtext-height, 320px) !important;
1007
273
  }
1008
274
 
1009
- .vm-richtext__content .tiptap.vm-richtext__prose {
1010
- min-height: 100%;
1011
- padding: 12px 14px;
1012
- outline: none;
275
+ :deep(.w-e-text-container [data-slate-editor]) {
276
+ min-height: calc(var(--vm-richtext-height, 320px) - 16px) !important;
1013
277
  color: var(--foreground);
1014
278
  font-size: 14px;
1015
279
  line-height: 1.65;
1016
280
 
1017
- p { margin: 0.45em 0; }
1018
281
  strong, b { font-weight: 800; }
282
+ }
283
+ </style>
1019
284
 
1020
- p.is-editor-empty:first-child::before {
1021
- content: attr(data-placeholder);
1022
- float: left;
1023
- height: 0;
1024
- pointer-events: none;
1025
- color: var(--muted-foreground);
1026
- }
1027
-
1028
- h2, h3 { margin: 0.75em 0 0.35em; font-weight: 650; line-height: 1.35; }
1029
- /* Tailwind / 全局 reset 会 list-style:none,必须显式恢复,否则只见缩进不见符号 */
1030
- ul {
1031
- margin: 0.45em 0;
1032
- padding-left: 1.5em;
1033
- list-style-type: disc;
1034
- list-style-position: outside;
1035
- }
1036
- ul ul { list-style-type: circle; }
1037
- ul ul ul { list-style-type: square; }
1038
- ol {
1039
- margin: 0.45em 0;
1040
- padding-left: 1.5em;
1041
- list-style-type: decimal;
1042
- list-style-position: outside;
1043
- }
1044
- ol ol { list-style-type: lower-alpha; }
1045
- ol ol ol { list-style-type: lower-roman; }
1046
- li {
1047
- display: list-item;
1048
- }
1049
- ul[data-type='taskList'] {
1050
- list-style: none;
1051
- padding-left: 0.25em;
1052
- }
1053
- ul[data-type='taskList'] li {
1054
- display: flex;
1055
- gap: 8px;
1056
- align-items: center;
1057
- }
1058
- ul[data-type='taskList'] li > label {
1059
- display: flex;
1060
- align-items: center;
1061
- flex-shrink: 0;
1062
- margin: 0;
1063
- }
1064
- ul[data-type='taskList'] li > div > p { margin: 0; }
1065
- blockquote { margin: 0.45em 0; padding-left: 12px; border-left: 3px solid var(--border); color: var(--muted-foreground); }
1066
- a { color: var(--brand); text-decoration: underline; cursor: pointer; }
1067
- img, video { display: block; max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
1068
- mark {
1069
- border-radius: 2px;
1070
- padding: 0 2px;
1071
- }
1072
- mark:not([style*='background']) {
1073
- background: color-mix(in srgb, var(--brand) 22%, transparent);
1074
- }
1075
- code { padding: 0.1em 0.35em; border-radius: 4px; background: var(--muted); font-size: 0.92em; }
1076
- pre { margin: 0.5em 0; padding: 10px 12px; border-radius: 8px; background: var(--muted); overflow-x: auto;
1077
- code { padding: 0; background: transparent; }
1078
- }
1079
- table { width: 100%; border-collapse: collapse; margin: 8px 0; table-layout: fixed; }
1080
- th, td {
1081
- position: relative;
1082
- min-width: 48px;
1083
- padding: 6px 8px;
1084
- border: 1px solid var(--border);
1085
- vertical-align: top;
1086
- }
1087
- th { background: var(--muted); font-weight: 600; }
1088
- .selectedCell {
1089
- background: color-mix(in srgb, var(--brand) 16%, transparent);
1090
- }
1091
- .selectedCell::after {
1092
- content: '';
1093
- position: absolute;
1094
- inset: 0;
1095
- pointer-events: none;
1096
- background: color-mix(in srgb, var(--brand) 10%, transparent);
1097
- }
1098
- .column-resize-handle {
1099
- position: absolute;
1100
- top: 0;
1101
- right: -2px;
1102
- bottom: 0;
1103
- width: 4px;
1104
- background: var(--brand);
1105
- pointer-events: none;
1106
- }
1107
- hr { margin: 12px 0; border: none; border-top: 1px solid var(--border); }
285
+ <style lang="scss">
286
+ /* wangEditor 浮层挂到 body,需高于 Dialog */
287
+ .w-e-bar-item-menus-container,
288
+ .w-e-select-list,
289
+ .w-e-drop-panel,
290
+ .w-e-modal,
291
+ .w-e-panel-content-emotion,
292
+ .w-e-panel-content-color,
293
+ .w-e-panel-content-table {
294
+ z-index: 10050 !important;
1108
295
  }
1109
296
  </style>