@mnykj/alleditor-dual 1.1.2 → 1.2.1
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/README.md +21 -0
- package/package.json +62 -59
- package/src/DualEditor.vue +167 -4
- package/src/defaultToolbar.ts +100 -0
- package/src/importMenus.ts +110 -0
- package/src/index.ts +17 -0
- package/src/markdown.ts +46 -0
- package/src/stripJavascriptFromHtml.ts +28 -0
package/README.md
CHANGED
|
@@ -49,6 +49,27 @@ const html = ref('<p>hello</p>')
|
|
|
49
49
|
</template>
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
默认工具栏已带 **导入 Markdown**、**导入 HTML**:
|
|
53
|
+
- `.md` → wang 引擎
|
|
54
|
+
- `.html` → ue 引擎(并剥离 script / 事件属性)
|
|
55
|
+
|
|
56
|
+
不需要时:`<DualEditor :enable-import="false" />`。自定义 `toolbarConfig.toolbarKeys` 时,若未包含这两项会自动补上(`enableImport` 为 true)。
|
|
57
|
+
|
|
58
|
+
仅注册菜单、自己接文件选择(如创作中心):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import {
|
|
62
|
+
registerImportMenus,
|
|
63
|
+
bindImportMenuHandlers,
|
|
64
|
+
} from '@mnykj/alleditor-dual'
|
|
65
|
+
|
|
66
|
+
registerImportMenus()
|
|
67
|
+
bindImportMenuHandlers({
|
|
68
|
+
onImportHtml: () => htmlInput.click(),
|
|
69
|
+
onImportMarkdown: () => mdInput.click(),
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
52
73
|
一般不用再写 `ueditor-base`。若自建 CDN,可覆盖:
|
|
53
74
|
|
|
54
75
|
```vue
|
package/package.json
CHANGED
|
@@ -1,59 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@mnykj/alleditor-dual",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"description": "AllEditor dual-engine (wang + UE HTML) Vue3 components, with bundled UEditor static assets",
|
|
5
|
-
"author": "mnykj",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"main": "./src/index.ts",
|
|
9
|
-
"module": "./src/index.ts",
|
|
10
|
-
"types": "./src/index.ts",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": "./src/index.ts",
|
|
13
|
-
"./UeHtmlEngine.vue": "./src/UeHtmlEngine.vue",
|
|
14
|
-
"./DualEditor.vue": "./src/DualEditor.vue",
|
|
15
|
-
"./vite": "./vite.ts",
|
|
16
|
-
"./nuxt": "./nuxt.ts",
|
|
17
|
-
"./ueditor/*": "./ueditor/*"
|
|
18
|
-
},
|
|
19
|
-
"files": [
|
|
20
|
-
"src",
|
|
21
|
-
"ueditor",
|
|
22
|
-
"vite.ts",
|
|
23
|
-
"nuxt.ts",
|
|
24
|
-
"README.md",
|
|
25
|
-
"scripts"
|
|
26
|
-
],
|
|
27
|
-
"keywords": [
|
|
28
|
-
"AllEditor",
|
|
29
|
-
"alleditor",
|
|
30
|
-
"dual-engine",
|
|
31
|
-
"ueditor",
|
|
32
|
-
"vue3",
|
|
33
|
-
"mnykj"
|
|
34
|
-
],
|
|
35
|
-
"publishConfig": {
|
|
36
|
-
"access": "public",
|
|
37
|
-
"registry": "https://registry.npmjs.org/"
|
|
38
|
-
},
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"@
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
"optional": true
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@mnykj/alleditor-dual",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "AllEditor dual-engine (wang + UE HTML) Vue3 components, with bundled UEditor static assets and MD/HTML import menus",
|
|
5
|
+
"author": "mnykj",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./src/index.ts",
|
|
9
|
+
"module": "./src/index.ts",
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.ts",
|
|
13
|
+
"./UeHtmlEngine.vue": "./src/UeHtmlEngine.vue",
|
|
14
|
+
"./DualEditor.vue": "./src/DualEditor.vue",
|
|
15
|
+
"./vite": "./vite.ts",
|
|
16
|
+
"./nuxt": "./nuxt.ts",
|
|
17
|
+
"./ueditor/*": "./ueditor/*"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"ueditor",
|
|
22
|
+
"vite.ts",
|
|
23
|
+
"nuxt.ts",
|
|
24
|
+
"README.md",
|
|
25
|
+
"scripts"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"AllEditor",
|
|
29
|
+
"alleditor",
|
|
30
|
+
"dual-engine",
|
|
31
|
+
"ueditor",
|
|
32
|
+
"vue3",
|
|
33
|
+
"mnykj"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"registry": "https://registry.npmjs.org/"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"markdown-it": "^14.3.3"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@mnykj/alleditor": "^1.1.1",
|
|
44
|
+
"@mnykj/alleditor-for-vue": "^1.1.0",
|
|
45
|
+
"@nuxt/kit": ">=3",
|
|
46
|
+
"vite": ">=4",
|
|
47
|
+
"vue": "^3.0.5"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"vite": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@nuxt/kit": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"sync:ueditor": "node ./scripts/sync-ueditor.mjs",
|
|
59
|
+
"prepack": "node ./scripts/sync-ueditor.mjs",
|
|
60
|
+
"build": "node -e \"console.log('@mnykj/alleditor-dual ships Vue/TS source + ueditor assets; no bundle step')\""
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/DualEditor.vue
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* AllEditor 双引擎门面:按 HTML 协议自动选 wang / ue,共用 wang 工具栏。
|
|
4
4
|
*
|
|
5
|
-
* v-model 存「可入库」HTML
|
|
5
|
+
* v-model 存「可入库」HTML(默认 wang/ue 都包 data-alleditor-engine 根节点)。
|
|
6
|
+
* 内置 MD / HTML 导入菜单(默认工具栏可见)。
|
|
6
7
|
* 宿主需自行提供 UEditor 静态资源(默认 /ueditor/)。
|
|
7
8
|
*/
|
|
8
9
|
import {
|
|
9
10
|
computed,
|
|
10
11
|
nextTick,
|
|
11
12
|
onBeforeUnmount,
|
|
13
|
+
onMounted,
|
|
12
14
|
ref,
|
|
13
15
|
shallowRef,
|
|
14
16
|
watch,
|
|
@@ -21,6 +23,17 @@ import {
|
|
|
21
23
|
} from '@mnykj/alleditor'
|
|
22
24
|
import UeHtmlEngine from './UeHtmlEngine.vue'
|
|
23
25
|
import { bindUeToolbarAdapter } from './ueToolbar'
|
|
26
|
+
import {
|
|
27
|
+
bindImportMenuHandlers,
|
|
28
|
+
registerImportMenus,
|
|
29
|
+
} from './importMenus'
|
|
30
|
+
import { mergeToolbarConfig } from './defaultToolbar'
|
|
31
|
+
import {
|
|
32
|
+
extractHtmlBody,
|
|
33
|
+
isEffectivelyEmptyHtml,
|
|
34
|
+
markdownToHtml,
|
|
35
|
+
} from './markdown'
|
|
36
|
+
import { stripJavascriptFromHtml } from './stripJavascriptFromHtml'
|
|
24
37
|
|
|
25
38
|
const props = defineProps({
|
|
26
39
|
modelValue: { type: String, default: '' },
|
|
@@ -33,6 +46,10 @@ const props = defineProps({
|
|
|
33
46
|
wangPlaceholder: { type: String, default: '<p><br></p>' },
|
|
34
47
|
/** 为 true 时仅 ue 写外层 div;默认 false,wang/ue 都包 data-alleditor-engine */
|
|
35
48
|
wrapOnlyWhenUe: { type: Boolean, default: false },
|
|
49
|
+
/** 是否注册并显示 MD/HTML 导入按钮(默认开启) */
|
|
50
|
+
enableImport: { type: Boolean, default: true },
|
|
51
|
+
/** 正文非空时导入是否 confirm 确认替换 */
|
|
52
|
+
confirmReplaceOnImport: { type: Boolean, default: true },
|
|
36
53
|
toolbarSelector: {
|
|
37
54
|
type: String,
|
|
38
55
|
default: '.alleditor-dual .w-e-toolbar, .alleditor-dual .editor-toolbar',
|
|
@@ -46,6 +63,7 @@ const emit = defineEmits([
|
|
|
46
63
|
'wang-created',
|
|
47
64
|
'ue-created',
|
|
48
65
|
'ue-ready',
|
|
66
|
+
'import',
|
|
49
67
|
])
|
|
50
68
|
|
|
51
69
|
/** @type {import('vue').Ref<'wang' | 'ue'>} */
|
|
@@ -53,13 +71,26 @@ const engineMode = ref(detectEditorEngine(props.modelValue))
|
|
|
53
71
|
const rootRef = ref(null)
|
|
54
72
|
const wangEditorRef = shallowRef(null)
|
|
55
73
|
const ueEngineRef = shallowRef(null)
|
|
56
|
-
|
|
57
|
-
const
|
|
74
|
+
// 首帧即用正文初始化,避免先挂占位再 setHtml 时带着失效选区炸 Slate
|
|
75
|
+
const wangHtml = ref(
|
|
76
|
+
engineMode.value === 'wang'
|
|
77
|
+
? unwrapEditorEngineHtml(props.modelValue || '') || props.wangPlaceholder
|
|
78
|
+
: props.wangPlaceholder
|
|
79
|
+
)
|
|
80
|
+
const ueHtml = ref(
|
|
81
|
+
engineMode.value === 'ue' ? unwrapEditorEngineHtml(props.modelValue || '') : ''
|
|
82
|
+
)
|
|
83
|
+
const mdFileInputRef = ref(null)
|
|
84
|
+
const htmlFileInputRef = ref(null)
|
|
58
85
|
let toolbarBinding = null
|
|
59
86
|
let applyingExternal = false
|
|
60
87
|
|
|
61
88
|
const useUe = computed(() => engineMode.value === 'ue')
|
|
62
89
|
|
|
90
|
+
const resolvedToolbarConfig = computed(() =>
|
|
91
|
+
mergeToolbarConfig(props.toolbarConfig, { enableImport: props.enableImport })
|
|
92
|
+
)
|
|
93
|
+
|
|
63
94
|
function bodyOf(html) {
|
|
64
95
|
return unwrapEditorEngineHtml(html || '') || '<p><br></p>'
|
|
65
96
|
}
|
|
@@ -180,6 +211,96 @@ function setEngineMode(mode, html) {
|
|
|
180
211
|
: body,
|
|
181
212
|
{ forceEngine: next }
|
|
182
213
|
)
|
|
214
|
+
if (next === 'ue' || next === 'wang') {
|
|
215
|
+
emitModel(body)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function currentBodyForEmptyCheck() {
|
|
220
|
+
return bodyOf(getHtml())
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function confirmReplaceIfNeeded() {
|
|
224
|
+
if (!props.confirmReplaceOnImport) return true
|
|
225
|
+
if (isEffectivelyEmptyHtml(currentBodyForEmptyCheck())) return true
|
|
226
|
+
return window.confirm('导入将替换当前正文内容,是否继续?')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function openMarkdownPicker() {
|
|
230
|
+
if (props.disabled) return
|
|
231
|
+
mdFileInputRef.value?.click?.()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function openHtmlPicker() {
|
|
235
|
+
if (props.disabled) return
|
|
236
|
+
htmlFileInputRef.value?.click?.()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function onMarkdownFileSelected(e) {
|
|
240
|
+
const file = e.target?.files?.[0]
|
|
241
|
+
if (e.target) e.target.value = ''
|
|
242
|
+
if (!file) return
|
|
243
|
+
if (!/\.md$/i.test(file.name)) {
|
|
244
|
+
window.alert('请选择 .md 文件')
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (!confirmReplaceIfNeeded()) return
|
|
248
|
+
try {
|
|
249
|
+
const text = await file.text()
|
|
250
|
+
const html = await markdownToHtml(text || '')
|
|
251
|
+
setEngineMode('wang', html)
|
|
252
|
+
emit('import', {
|
|
253
|
+
type: 'markdown',
|
|
254
|
+
fileName: file.name,
|
|
255
|
+
html: getHtml(),
|
|
256
|
+
})
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error(err)
|
|
259
|
+
window.alert(err?.message || '导入 Markdown 失败')
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function onHtmlFileSelected(e) {
|
|
264
|
+
const file = e.target?.files?.[0]
|
|
265
|
+
if (e.target) e.target.value = ''
|
|
266
|
+
if (!file) return
|
|
267
|
+
const nameOk =
|
|
268
|
+
/\.html?$/i.test(file.name) ||
|
|
269
|
+
/text\/html/i.test(file.type || '')
|
|
270
|
+
if (!nameOk) {
|
|
271
|
+
window.alert('请选择 .html / .htm 文件')
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
if (!confirmReplaceIfNeeded()) return
|
|
275
|
+
try {
|
|
276
|
+
const text = await file.text()
|
|
277
|
+
const body = stripJavascriptFromHtml(extractHtmlBody(text || ''))
|
|
278
|
+
const html = body.trim() || '<p><br></p>'
|
|
279
|
+
setEngineMode('ue', html)
|
|
280
|
+
emit('import', {
|
|
281
|
+
type: 'html',
|
|
282
|
+
fileName: file.name,
|
|
283
|
+
html: getHtml(),
|
|
284
|
+
})
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error(err)
|
|
287
|
+
window.alert(err?.message || '导入 HTML 失败')
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function wireImportHandlers() {
|
|
292
|
+
if (!props.enableImport) {
|
|
293
|
+
bindImportMenuHandlers({
|
|
294
|
+
onImportHtml: null,
|
|
295
|
+
onImportMarkdown: null,
|
|
296
|
+
})
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
registerImportMenus()
|
|
300
|
+
bindImportMenuHandlers({
|
|
301
|
+
onImportHtml: openHtmlPicker,
|
|
302
|
+
onImportMarkdown: openMarkdownPicker,
|
|
303
|
+
})
|
|
183
304
|
}
|
|
184
305
|
|
|
185
306
|
watch(
|
|
@@ -207,10 +328,23 @@ watch(engineMode, (mode) => {
|
|
|
207
328
|
else unbindToolbar()
|
|
208
329
|
})
|
|
209
330
|
|
|
331
|
+
watch(
|
|
332
|
+
() => props.enableImport,
|
|
333
|
+
() => wireImportHandlers()
|
|
334
|
+
)
|
|
335
|
+
|
|
210
336
|
applyIncoming(props.modelValue)
|
|
211
337
|
|
|
338
|
+
onMounted(() => {
|
|
339
|
+
wireImportHandlers()
|
|
340
|
+
})
|
|
341
|
+
|
|
212
342
|
onBeforeUnmount(() => {
|
|
213
343
|
unbindToolbar()
|
|
344
|
+
bindImportMenuHandlers({
|
|
345
|
+
onImportHtml: null,
|
|
346
|
+
onImportMarkdown: null,
|
|
347
|
+
})
|
|
214
348
|
const ed = wangEditorRef.value
|
|
215
349
|
if (ed && !ed.isDestroyed) {
|
|
216
350
|
try {
|
|
@@ -228,6 +362,8 @@ defineExpose({
|
|
|
228
362
|
setEngineMode,
|
|
229
363
|
getWangEditor: () => wangEditorRef.value,
|
|
230
364
|
getUeEngine: () => ueEngineRef.value,
|
|
365
|
+
openMarkdownPicker,
|
|
366
|
+
openHtmlPicker,
|
|
231
367
|
engineMode,
|
|
232
368
|
})
|
|
233
369
|
</script>
|
|
@@ -237,7 +373,7 @@ defineExpose({
|
|
|
237
373
|
<Toolbar
|
|
238
374
|
v-if="wangEditorRef"
|
|
239
375
|
:editor="wangEditorRef"
|
|
240
|
-
:default-config="
|
|
376
|
+
:default-config="resolvedToolbarConfig"
|
|
241
377
|
:mode="mode"
|
|
242
378
|
class="alleditor-dual__toolbar editor-toolbar"
|
|
243
379
|
/>
|
|
@@ -266,6 +402,22 @@ defineExpose({
|
|
|
266
402
|
/>
|
|
267
403
|
</div>
|
|
268
404
|
</div>
|
|
405
|
+
<input
|
|
406
|
+
v-if="enableImport"
|
|
407
|
+
ref="mdFileInputRef"
|
|
408
|
+
type="file"
|
|
409
|
+
accept=".md,text/markdown"
|
|
410
|
+
class="alleditor-dual__file-input"
|
|
411
|
+
@change="onMarkdownFileSelected"
|
|
412
|
+
/>
|
|
413
|
+
<input
|
|
414
|
+
v-if="enableImport"
|
|
415
|
+
ref="htmlFileInputRef"
|
|
416
|
+
type="file"
|
|
417
|
+
accept=".html,.htm,text/html"
|
|
418
|
+
class="alleditor-dual__file-input"
|
|
419
|
+
@change="onHtmlFileSelected"
|
|
420
|
+
/>
|
|
269
421
|
</div>
|
|
270
422
|
</template>
|
|
271
423
|
|
|
@@ -327,4 +479,15 @@ defineExpose({
|
|
|
327
479
|
min-height: 0;
|
|
328
480
|
height: 100%;
|
|
329
481
|
}
|
|
482
|
+
|
|
483
|
+
.alleditor-dual__file-input {
|
|
484
|
+
position: absolute !important;
|
|
485
|
+
width: 1px !important;
|
|
486
|
+
height: 1px !important;
|
|
487
|
+
padding: 0 !important;
|
|
488
|
+
margin: -1px !important;
|
|
489
|
+
overflow: hidden !important;
|
|
490
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
491
|
+
border: 0 !important;
|
|
492
|
+
}
|
|
330
493
|
</style>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DualEditor 默认工具栏:AllEditor 标准项 + MD/HTML 导入。
|
|
3
|
+
* 宿主传入 toolbarConfig.toolbarKeys 时可完全覆盖。
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_DUAL_TOOLBAR_KEYS: Array<string | Record<string, any>> = [
|
|
6
|
+
'headerSelect',
|
|
7
|
+
'blockquote',
|
|
8
|
+
'|',
|
|
9
|
+
'bold',
|
|
10
|
+
'underline',
|
|
11
|
+
'italic',
|
|
12
|
+
{
|
|
13
|
+
key: 'group-more-style',
|
|
14
|
+
title: '更多',
|
|
15
|
+
iconSvg:
|
|
16
|
+
'<svg viewBox="0 0 1024 1024"><path d="M204.8 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M505.6 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M806.4 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path></svg>',
|
|
17
|
+
menuKeys: ['through', 'code', 'sup', 'sub', 'clearStyle'],
|
|
18
|
+
},
|
|
19
|
+
'color',
|
|
20
|
+
'bgColor',
|
|
21
|
+
'|',
|
|
22
|
+
'fontSize',
|
|
23
|
+
'fontFamily',
|
|
24
|
+
'lineHeight',
|
|
25
|
+
'|',
|
|
26
|
+
'bulletedList',
|
|
27
|
+
'numberedList',
|
|
28
|
+
'todo',
|
|
29
|
+
{
|
|
30
|
+
key: 'group-justify',
|
|
31
|
+
title: '对齐',
|
|
32
|
+
iconSvg:
|
|
33
|
+
'<svg viewBox="0 0 1024 1024"><path d="M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>',
|
|
34
|
+
menuKeys: ['justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'group-indent',
|
|
38
|
+
title: '缩进',
|
|
39
|
+
iconSvg:
|
|
40
|
+
'<svg viewBox="0 0 1024 1024"><path d="M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m0-128V320l256 192z"></path></svg>',
|
|
41
|
+
menuKeys: ['indent', 'delIndent'],
|
|
42
|
+
},
|
|
43
|
+
'|',
|
|
44
|
+
'emotion',
|
|
45
|
+
'insertLink',
|
|
46
|
+
{
|
|
47
|
+
key: 'group-image',
|
|
48
|
+
title: '图片',
|
|
49
|
+
iconSvg:
|
|
50
|
+
'<svg viewBox="0 0 1024 1024"><path d="M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z"></path></svg>',
|
|
51
|
+
menuKeys: ['insertImage', 'uploadImage'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'group-video',
|
|
55
|
+
title: '视频',
|
|
56
|
+
iconSvg:
|
|
57
|
+
'<svg viewBox="0 0 1024 1024"><path d="M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z"></path></svg>',
|
|
58
|
+
menuKeys: ['insertVideo', 'uploadVideo'],
|
|
59
|
+
},
|
|
60
|
+
'insertTable',
|
|
61
|
+
'codeBlock',
|
|
62
|
+
'divider',
|
|
63
|
+
'|',
|
|
64
|
+
'undo',
|
|
65
|
+
'redo',
|
|
66
|
+
'|',
|
|
67
|
+
'importMarkdown',
|
|
68
|
+
'sourceCode',
|
|
69
|
+
'|',
|
|
70
|
+
'fullScreen',
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
export function mergeToolbarConfig(
|
|
74
|
+
userConfig: Record<string, any> | null | undefined,
|
|
75
|
+
options?: { enableImport?: boolean }
|
|
76
|
+
) {
|
|
77
|
+
const enableImport = options?.enableImport !== false
|
|
78
|
+
const user = userConfig && typeof userConfig === 'object' ? { ...userConfig } : {}
|
|
79
|
+
if (Array.isArray(user.toolbarKeys) && user.toolbarKeys.length) {
|
|
80
|
+
if (!enableImport) return user
|
|
81
|
+
const keys = user.toolbarKeys
|
|
82
|
+
const hasMd = keys.some(
|
|
83
|
+
(k: any) => k === 'importMarkdown' || k?.key === 'importMarkdown'
|
|
84
|
+
)
|
|
85
|
+
const hasHtml = keys.some(
|
|
86
|
+
(k: any) => k === 'sourceCode' || k?.key === 'sourceCode'
|
|
87
|
+
)
|
|
88
|
+
if (hasMd && hasHtml) return user
|
|
89
|
+
const next = [...keys]
|
|
90
|
+
if (!hasMd) next.push('importMarkdown')
|
|
91
|
+
if (!hasHtml) next.push('sourceCode')
|
|
92
|
+
return { ...user, toolbarKeys: next }
|
|
93
|
+
}
|
|
94
|
+
const keys = enableImport
|
|
95
|
+
? DEFAULT_DUAL_TOOLBAR_KEYS
|
|
96
|
+
: DEFAULT_DUAL_TOOLBAR_KEYS.filter(
|
|
97
|
+
(k) => k !== 'importMarkdown' && k !== 'sourceCode'
|
|
98
|
+
)
|
|
99
|
+
return { ...user, toolbarKeys: keys }
|
|
100
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Boot } from '@mnykj/alleditor'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 工具栏「HTML」:导入 .html(外观对齐百度 UEditor)。
|
|
5
|
+
* 实际打开文件选择由 SourceCodeMenu.attrs.onImport 接到宿主。
|
|
6
|
+
*/
|
|
7
|
+
export class SourceCodeMenu {
|
|
8
|
+
title = '导入 HTML'
|
|
9
|
+
tag = 'button'
|
|
10
|
+
iconSvg =
|
|
11
|
+
'<svg viewBox="0 0 44 24" width="44" height="24"><text x="22" y="16" text-anchor="middle" font-size="12" font-weight="700" font-family="Segoe UI,Helvetica,Arial,sans-serif" fill="#1a73e8">HTML</text></svg>'
|
|
12
|
+
|
|
13
|
+
getValue() {
|
|
14
|
+
return ''
|
|
15
|
+
}
|
|
16
|
+
isActive() {
|
|
17
|
+
return !!(SourceCodeMenu.attrs as { active?: boolean }).active
|
|
18
|
+
}
|
|
19
|
+
isDisabled() {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
exec() {
|
|
23
|
+
const handler = SourceCodeMenu.attrs.onImport
|
|
24
|
+
if (typeof handler === 'function') handler()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static attrs: {
|
|
28
|
+
active: boolean
|
|
29
|
+
onImport: null | (() => void)
|
|
30
|
+
onToggle: null | (() => void)
|
|
31
|
+
} = {
|
|
32
|
+
active: false,
|
|
33
|
+
onImport: null,
|
|
34
|
+
onToggle: null,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 工具栏:导入 Markdown 文件(仅 .md)。
|
|
40
|
+
*/
|
|
41
|
+
export class ImportMarkdownMenu {
|
|
42
|
+
title = '导入 Markdown'
|
|
43
|
+
tag = 'button'
|
|
44
|
+
iconSvg =
|
|
45
|
+
'<svg viewBox="0 0 16 16"><path d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"></path></svg>'
|
|
46
|
+
|
|
47
|
+
getValue() {
|
|
48
|
+
return ''
|
|
49
|
+
}
|
|
50
|
+
isActive() {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
isDisabled() {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
exec() {
|
|
57
|
+
const handler = ImportMarkdownMenu.attrs.onImport
|
|
58
|
+
if (typeof handler === 'function') handler()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static attrs: {
|
|
62
|
+
onImport: null | (() => void)
|
|
63
|
+
} = {
|
|
64
|
+
onImport: null,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let menusRegistered = false
|
|
69
|
+
|
|
70
|
+
/** 注册导入菜单(幂等;HMR 安全) */
|
|
71
|
+
export function registerImportMenus() {
|
|
72
|
+
if (menusRegistered) return
|
|
73
|
+
try {
|
|
74
|
+
Boot.registerMenu({
|
|
75
|
+
key: 'sourceCode',
|
|
76
|
+
factory() {
|
|
77
|
+
return new SourceCodeMenu()
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
} catch {
|
|
81
|
+
/* 重复注册忽略 */
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
Boot.registerMenu({
|
|
85
|
+
key: 'importMarkdown',
|
|
86
|
+
factory() {
|
|
87
|
+
return new ImportMarkdownMenu()
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
} catch {
|
|
91
|
+
/* 重复注册忽略 */
|
|
92
|
+
}
|
|
93
|
+
menusRegistered = true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function bindImportMenuHandlers(handlers: {
|
|
97
|
+
onImportHtml?: null | (() => void)
|
|
98
|
+
onImportMarkdown?: null | (() => void)
|
|
99
|
+
onToggleSource?: null | (() => void)
|
|
100
|
+
}) {
|
|
101
|
+
if (handlers.onImportHtml !== undefined) {
|
|
102
|
+
SourceCodeMenu.attrs.onImport = handlers.onImportHtml
|
|
103
|
+
}
|
|
104
|
+
if (handlers.onToggleSource !== undefined) {
|
|
105
|
+
SourceCodeMenu.attrs.onToggle = handlers.onToggleSource
|
|
106
|
+
}
|
|
107
|
+
if (handlers.onImportMarkdown !== undefined) {
|
|
108
|
+
ImportMarkdownMenu.attrs.onImport = handlers.onImportMarkdown
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,23 @@ export type { UeCommandRunner, BindUeToolbarOptions, UeToolbarBinding } from './
|
|
|
16
16
|
|
|
17
17
|
export { DEFAULT_UEDITOR_PUBLIC_BASE } from './ueditorBase'
|
|
18
18
|
|
|
19
|
+
export {
|
|
20
|
+
SourceCodeMenu,
|
|
21
|
+
ImportMarkdownMenu,
|
|
22
|
+
registerImportMenus,
|
|
23
|
+
bindImportMenuHandlers,
|
|
24
|
+
} from './importMenus'
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_DUAL_TOOLBAR_KEYS,
|
|
27
|
+
mergeToolbarConfig,
|
|
28
|
+
} from './defaultToolbar'
|
|
29
|
+
export {
|
|
30
|
+
markdownToHtml,
|
|
31
|
+
extractHtmlBody,
|
|
32
|
+
isEffectivelyEmptyHtml,
|
|
33
|
+
} from './markdown'
|
|
34
|
+
export { stripJavascriptFromHtml } from './stripJavascriptFromHtml'
|
|
35
|
+
|
|
19
36
|
// 协议工具从核心包再导出,方便只装 dual 的调用方
|
|
20
37
|
export type { AllEditorEngine } from '@mnykj/alleditor'
|
|
21
38
|
export {
|
package/src/markdown.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** @type {import('markdown-it') | null} */
|
|
2
|
+
let markdownItInstance: any = null
|
|
3
|
+
|
|
4
|
+
export async function markdownToHtml(text: string): Promise<string> {
|
|
5
|
+
if (!markdownItInstance) {
|
|
6
|
+
const mod = await import('markdown-it')
|
|
7
|
+
const MarkdownIt = mod.default || mod
|
|
8
|
+
markdownItInstance = new MarkdownIt({
|
|
9
|
+
html: false,
|
|
10
|
+
linkify: true,
|
|
11
|
+
breaks: true,
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
let html = markdownItInstance.render(text || '')
|
|
15
|
+
if (!String(html).trim()) html = '<p><br></p>'
|
|
16
|
+
return html
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 从完整 HTML 文档取出 body 内片段 */
|
|
20
|
+
export function extractHtmlBody(text: string): string {
|
|
21
|
+
let s = String(text || '').replace(/^\uFEFF/, '')
|
|
22
|
+
const bodyMatch = s.match(/<body[^>]*>([\s\S]*?)<\/body>/i)
|
|
23
|
+
if (bodyMatch) s = bodyMatch[1]
|
|
24
|
+
return s.trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isEffectivelyEmptyHtml(html: string): boolean {
|
|
28
|
+
const s = String(html ?? '').trim()
|
|
29
|
+
if (!s) return true
|
|
30
|
+
if (
|
|
31
|
+
/<(img|video|audio|iframe|embed|object|source|hr|table|svg|blockquote|pre|ul|ol|h[1-6])\b/i.test(
|
|
32
|
+
s
|
|
33
|
+
)
|
|
34
|
+
) {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
const text = s
|
|
38
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
39
|
+
.replace(/ /gi, ' ')
|
|
40
|
+
.replace(/<br\s*\/?>/gi, '')
|
|
41
|
+
.replace(/<p>\s*<\/p>/gi, '')
|
|
42
|
+
.replace(/<div[^>]*>\s*<\/div>/gi, '')
|
|
43
|
+
.replace(/<[^>]+>/g, '')
|
|
44
|
+
.trim()
|
|
45
|
+
return text === ''
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导入 HTML 时剥离 JS:script、事件属性、javascript: 伪协议。
|
|
3
|
+
*/
|
|
4
|
+
export function stripJavascriptFromHtml(html: string): string {
|
|
5
|
+
let s = String(html ?? '')
|
|
6
|
+
|
|
7
|
+
const open = '<' + 'script'
|
|
8
|
+
const close = '<' + '/' + 'script>'
|
|
9
|
+
const blockRe = new RegExp(`${open}\\b[^>]*>[\\s\\S]*?${close}`, 'gi')
|
|
10
|
+
const selfRe = new RegExp(`${open}\\b[^>]*\\/?>`, 'gi')
|
|
11
|
+
|
|
12
|
+
s = s.replace(blockRe, '')
|
|
13
|
+
s = s.replace(selfRe, '')
|
|
14
|
+
|
|
15
|
+
s = s.replace(/\s+on[a-zA-Z]+\s*=\s*(['"])[\s\S]*?\1/g, '')
|
|
16
|
+
s = s.replace(/\s+on[a-zA-Z]+\s*=\s*[^\s>]+/g, '')
|
|
17
|
+
|
|
18
|
+
s = s.replace(
|
|
19
|
+
/\s(href|src|action|formaction|xlink:href)\s*=\s*(['"])\s*javascript:[\s\S]*?\2/gi,
|
|
20
|
+
' $1=$2#$2'
|
|
21
|
+
)
|
|
22
|
+
s = s.replace(
|
|
23
|
+
/\s(href|src|action|formaction|xlink:href)\s*=\s*(['"])?\s*javascript:[^\s>]*/gi,
|
|
24
|
+
' $1="#"'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return s
|
|
28
|
+
}
|