@longhongguo/form-create-ant-design-vue 3.3.4 → 3.3.7
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/auto-import.js +3 -1
- package/dist/form-create.css +432 -0
- package/dist/form-create.esm.css +432 -0
- package/dist/form-create.esm.js +2 -2
- package/dist/form-create.esm.js.map +1 -1
- package/dist/form-create.js +2 -2
- package/dist/form-create.js.map +1 -1
- package/package.json +2 -2
- package/src/components/FcEditorWrapper.vue +618 -2
- package/src/core/alias.js +3 -0
- package/src/core/api.js +26 -1
- package/src/core/manager.js +31 -0
- package/src/parsers/accTable.js +51 -19
- package/src/parsers/alert.js +17 -0
- package/src/parsers/cascader.js +69 -18
- package/src/parsers/index.js +2 -0
- package/src/parsers/input.js +61 -0
- package/src/style/index.css +432 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longhongguo/form-create-ant-design-vue",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.07",
|
|
4
4
|
"description": "AntDesignVue版本低代码表单|FormCreate 是一个可以通过 JSON 生成具有动态渲染、数据收集、验证和提交功能的低代码表单生成组件。支持6个UI框架,适配移动端,并且支持生成任何 Vue 组件。内置20种常用表单组件和自定义组件,再复杂的表单都可以轻松搞定。",
|
|
5
5
|
"main": "./dist/form-create.min.js",
|
|
6
6
|
"module": "./dist/form-create.esm.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@form-create/component-subform": "^3.1.34",
|
|
55
55
|
"@form-create/core": "^3.2.33",
|
|
56
56
|
"@form-create/utils": "^3.2.31",
|
|
57
|
-
"@longhongguo/component-antdv-upload": "^3.2.
|
|
57
|
+
"@longhongguo/component-antdv-upload": "^3.2.42",
|
|
58
58
|
"@longhongguo/form-create-core": "^3.2.63",
|
|
59
59
|
"moment": "^2.30.1"
|
|
60
60
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<FcEditor
|
|
3
3
|
:model-value="modelValue"
|
|
4
|
-
:disabled="disabled"
|
|
4
|
+
:disabled="disabled && !readOnly"
|
|
5
|
+
:readOnly="readOnly"
|
|
5
6
|
:config="editorConfig"
|
|
6
7
|
:init="initEditor"
|
|
7
8
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
@@ -21,6 +22,7 @@ export default defineComponent({
|
|
|
21
22
|
props: {
|
|
22
23
|
modelValue: String,
|
|
23
24
|
disabled: Boolean,
|
|
25
|
+
readOnly: Boolean,
|
|
24
26
|
// 图片上传相关配置
|
|
25
27
|
uploadImgServer: String,
|
|
26
28
|
uploadImgFieldName: {
|
|
@@ -37,10 +39,30 @@ export default defineComponent({
|
|
|
37
39
|
formCreateInject: Object
|
|
38
40
|
},
|
|
39
41
|
emits: ['update:modelValue'],
|
|
42
|
+
watch: {
|
|
43
|
+
readOnly(newVal) {
|
|
44
|
+
// 当只读状态变化时,更新编辑器状态
|
|
45
|
+
if (this._editor) {
|
|
46
|
+
this.setReadOnlyMode(newVal)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
40
50
|
computed: {
|
|
41
51
|
editorConfig() {
|
|
42
52
|
const config = {}
|
|
43
53
|
|
|
54
|
+
// 如果设置了 readOnly,配置只读模式
|
|
55
|
+
if (this.readOnly) {
|
|
56
|
+
// readOnly: true // 启用只读模式
|
|
57
|
+
// 注意:实际只读效果主要通过 setReadOnlyMode 方法实现
|
|
58
|
+
// (设置 contenteditable="false" 和阻止编辑事件)
|
|
59
|
+
// 只读模式允许复制文本和点击链接,但禁止编辑
|
|
60
|
+
config.readOnly = true
|
|
61
|
+
// 禁用自动聚焦,避免在只读模式下获得焦点
|
|
62
|
+
config.focus = false
|
|
63
|
+
config.autoFocus = false
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
// 如果设置了 uploadImgServer,配置图片上传
|
|
45
67
|
if (this.uploadImgServer) {
|
|
46
68
|
// wangeditor 4.x 使用这些配置项
|
|
@@ -154,9 +176,603 @@ export default defineComponent({
|
|
|
154
176
|
return config
|
|
155
177
|
}
|
|
156
178
|
},
|
|
179
|
+
beforeUnmount() {
|
|
180
|
+
// 清理粘贴事件监听器
|
|
181
|
+
if (this._pasteHandler && this._pasteHandlerContainer) {
|
|
182
|
+
this._pasteHandlerContainer.removeEventListener(
|
|
183
|
+
'paste',
|
|
184
|
+
this._pasteHandler,
|
|
185
|
+
true
|
|
186
|
+
)
|
|
187
|
+
this._pasteHandler = null
|
|
188
|
+
this._pasteHandlerContainer = null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 清理只读模式相关的监听器和定时器
|
|
192
|
+
if (this._editor) {
|
|
193
|
+
this.$nextTick(() => {
|
|
194
|
+
let textContainer = null
|
|
195
|
+
if (this._editor.$textElem && this._editor.$textElem[0]) {
|
|
196
|
+
textContainer = this._editor.$textElem[0]
|
|
197
|
+
} else {
|
|
198
|
+
const editorId = this._editor.id
|
|
199
|
+
if (editorId) {
|
|
200
|
+
const editorEl = document.getElementById(editorId)
|
|
201
|
+
if (editorEl) {
|
|
202
|
+
textContainer = editorEl.querySelector('.w-e-text')
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (textContainer) {
|
|
208
|
+
// 清除定时器
|
|
209
|
+
if (textContainer._readOnlyInterval) {
|
|
210
|
+
clearInterval(textContainer._readOnlyInterval)
|
|
211
|
+
delete textContainer._readOnlyInterval
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 停止观察器
|
|
215
|
+
if (textContainer._readOnlyAttributeObserver) {
|
|
216
|
+
textContainer._readOnlyAttributeObserver.disconnect()
|
|
217
|
+
delete textContainer._readOnlyAttributeObserver
|
|
218
|
+
}
|
|
219
|
+
if (textContainer._readOnlyLinkObserver) {
|
|
220
|
+
textContainer._readOnlyLinkObserver.disconnect()
|
|
221
|
+
delete textContainer._readOnlyLinkObserver
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 移除事件监听器
|
|
225
|
+
if (textContainer._readOnlyHandlers) {
|
|
226
|
+
const handlers = textContainer._readOnlyHandlers
|
|
227
|
+
if (handlers.preventEdit) {
|
|
228
|
+
textContainer.removeEventListener(
|
|
229
|
+
'keydown',
|
|
230
|
+
handlers.preventEdit,
|
|
231
|
+
true
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
if (handlers.preventAllEdit) {
|
|
235
|
+
textContainer.removeEventListener(
|
|
236
|
+
'keypress',
|
|
237
|
+
handlers.preventAllEdit,
|
|
238
|
+
true
|
|
239
|
+
)
|
|
240
|
+
textContainer.removeEventListener(
|
|
241
|
+
'paste',
|
|
242
|
+
handlers.preventAllEdit,
|
|
243
|
+
true
|
|
244
|
+
)
|
|
245
|
+
textContainer.removeEventListener(
|
|
246
|
+
'drop',
|
|
247
|
+
handlers.preventAllEdit,
|
|
248
|
+
true
|
|
249
|
+
)
|
|
250
|
+
textContainer.removeEventListener(
|
|
251
|
+
'input',
|
|
252
|
+
handlers.preventAllEdit,
|
|
253
|
+
true
|
|
254
|
+
)
|
|
255
|
+
textContainer.removeEventListener(
|
|
256
|
+
'beforeinput',
|
|
257
|
+
handlers.preventAllEdit,
|
|
258
|
+
true
|
|
259
|
+
)
|
|
260
|
+
textContainer.removeEventListener(
|
|
261
|
+
'compositionstart',
|
|
262
|
+
handlers.preventAllEdit,
|
|
263
|
+
true
|
|
264
|
+
)
|
|
265
|
+
textContainer.removeEventListener(
|
|
266
|
+
'compositionupdate',
|
|
267
|
+
handlers.preventAllEdit,
|
|
268
|
+
true
|
|
269
|
+
)
|
|
270
|
+
textContainer.removeEventListener(
|
|
271
|
+
'compositionend',
|
|
272
|
+
handlers.preventAllEdit,
|
|
273
|
+
true
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
delete textContainer._readOnlyHandlers
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
},
|
|
157
282
|
methods: {
|
|
158
283
|
initEditor(editor) {
|
|
159
|
-
|
|
284
|
+
if (!editor) return
|
|
285
|
+
|
|
286
|
+
// 保存编辑器引用
|
|
287
|
+
this._editor = editor
|
|
288
|
+
|
|
289
|
+
// 如果设置了只读模式,配置编辑器为只读
|
|
290
|
+
// 使用多重延迟和监听,确保编辑器完全创建后再设置
|
|
291
|
+
if (this.readOnly) {
|
|
292
|
+
// 立即设置一次
|
|
293
|
+
this.$nextTick(() => {
|
|
294
|
+
this.setReadOnlyMode(true)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// 第一次延迟:等待编辑器 DOM 创建
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
this.setReadOnlyMode(true)
|
|
300
|
+
// 第二次延迟:确保设置生效
|
|
301
|
+
setTimeout(() => {
|
|
302
|
+
this.setReadOnlyMode(true)
|
|
303
|
+
// 第三次延迟:确保完全生效
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
this.setReadOnlyMode(true)
|
|
306
|
+
}, 300)
|
|
307
|
+
}, 200)
|
|
308
|
+
}, 100)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 等待编辑器完全创建后,监听粘贴事件
|
|
312
|
+
this.$nextTick(() => {
|
|
313
|
+
// 尝试多种方式查找编辑器容器
|
|
314
|
+
let editorContainer = null
|
|
315
|
+
|
|
316
|
+
// 方式1: 通过编辑器实例属性查找
|
|
317
|
+
if (editor.$textContainerElem && editor.$textContainerElem[0]) {
|
|
318
|
+
editorContainer = editor.$textContainerElem[0]
|
|
319
|
+
} else if (editor.$textElem && editor.$textElem[0]) {
|
|
320
|
+
editorContainer = editor.$textElem[0]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 方式2: 通过编辑器 ID 查找
|
|
324
|
+
if (!editorContainer && editor.id) {
|
|
325
|
+
const editorEl = document.getElementById(editor.id)
|
|
326
|
+
if (editorEl) {
|
|
327
|
+
editorContainer =
|
|
328
|
+
editorEl.querySelector('.w-e-text-container') ||
|
|
329
|
+
editorEl.querySelector('.w-e-text')
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 方式3: 通过 class 查找(通用方法)
|
|
334
|
+
if (!editorContainer) {
|
|
335
|
+
const textContainer = document.querySelector('.w-e-text-container')
|
|
336
|
+
if (textContainer) {
|
|
337
|
+
editorContainer = textContainer
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (editorContainer) {
|
|
342
|
+
this.setupPasteHandler(editor, editorContainer)
|
|
343
|
+
} else {
|
|
344
|
+
// 延迟重试,等待编辑器完全渲染
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
const textContainer =
|
|
347
|
+
document.querySelector('.w-e-text-container') ||
|
|
348
|
+
document.querySelector('.w-e-text')
|
|
349
|
+
if (textContainer && editor) {
|
|
350
|
+
this.setupPasteHandler(editor, textContainer)
|
|
351
|
+
} else {
|
|
352
|
+
console.warn(
|
|
353
|
+
'无法找到 wangEditor 文本容器,链接自动转换功能可能无法正常工作'
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
}, 500)
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
},
|
|
360
|
+
// 设置粘贴事件处理器
|
|
361
|
+
setupPasteHandler(editor, container) {
|
|
362
|
+
const handlePaste = (event) => {
|
|
363
|
+
// 获取粘贴的文本内容
|
|
364
|
+
const pasteText = (
|
|
365
|
+
event.clipboardData || window.clipboardData
|
|
366
|
+
)?.getData('text')
|
|
367
|
+
|
|
368
|
+
// 检查是否为纯链接(URL格式)
|
|
369
|
+
if (pasteText && this.isValidUrl(pasteText.trim())) {
|
|
370
|
+
// 阻止默认粘贴行为
|
|
371
|
+
event.preventDefault()
|
|
372
|
+
event.stopPropagation()
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const url = pasteText.trim()
|
|
376
|
+
// 确保 URL 有协议
|
|
377
|
+
const fullUrl =
|
|
378
|
+
url.startsWith('http://') || url.startsWith('https://')
|
|
379
|
+
? url
|
|
380
|
+
: `http://${url}`
|
|
381
|
+
|
|
382
|
+
// 尝试使用 wangEditor 的命令插入链接
|
|
383
|
+
// wangEditor 4.x 支持通过 cmd.do 执行命令
|
|
384
|
+
if (editor.cmd && editor.cmd.do) {
|
|
385
|
+
try {
|
|
386
|
+
// 先插入文本,然后选中并转换为链接
|
|
387
|
+
editor.cmd.do(
|
|
388
|
+
'insertHTML',
|
|
389
|
+
`<a href="${fullUrl}" target="_blank" rel="noopener noreferrer">${url}</a>`
|
|
390
|
+
)
|
|
391
|
+
return false
|
|
392
|
+
} catch (e) {
|
|
393
|
+
// 如果 cmd.do 失败,继续使用 DOM 方式
|
|
394
|
+
console.debug('使用 cmd.do 插入链接失败,尝试 DOM 方式:', e)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 使用 DOM 方式插入链接
|
|
399
|
+
const selection = window.getSelection()
|
|
400
|
+
if (selection && selection.rangeCount > 0) {
|
|
401
|
+
const range = selection.getRangeAt(0)
|
|
402
|
+
|
|
403
|
+
// 删除选中的内容
|
|
404
|
+
range.deleteContents()
|
|
405
|
+
|
|
406
|
+
// 创建链接元素
|
|
407
|
+
const linkElement = document.createElement('a')
|
|
408
|
+
linkElement.href = fullUrl
|
|
409
|
+
linkElement.textContent = url
|
|
410
|
+
linkElement.target = '_blank'
|
|
411
|
+
linkElement.rel = 'noopener noreferrer'
|
|
412
|
+
|
|
413
|
+
// 插入链接
|
|
414
|
+
range.insertNode(linkElement)
|
|
415
|
+
|
|
416
|
+
// 移动光标到链接后面,并添加一个空格
|
|
417
|
+
const textNode = document.createTextNode(' ')
|
|
418
|
+
range.setStartAfter(linkElement)
|
|
419
|
+
range.insertNode(textNode)
|
|
420
|
+
range.setStartAfter(textNode)
|
|
421
|
+
range.collapse(true)
|
|
422
|
+
selection.removeAllRanges()
|
|
423
|
+
selection.addRange(range)
|
|
424
|
+
|
|
425
|
+
// 触发编辑器内容变化
|
|
426
|
+
// 尝试多种方式触发更新
|
|
427
|
+
if (editor.txt) {
|
|
428
|
+
// 方式1: 触发 change 事件
|
|
429
|
+
if (
|
|
430
|
+
editor.txt.eventHooks &&
|
|
431
|
+
editor.txt.eventHooks.changeEvents
|
|
432
|
+
) {
|
|
433
|
+
editor.txt.eventHooks.changeEvents.forEach((fn) => {
|
|
434
|
+
if (typeof fn === 'function') {
|
|
435
|
+
try {
|
|
436
|
+
fn()
|
|
437
|
+
} catch (e) {
|
|
438
|
+
console.debug('触发编辑器变化事件失败:', e)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 方式2: 手动触发 input 事件
|
|
445
|
+
const inputEvent = new Event('input', {
|
|
446
|
+
bubbles: true,
|
|
447
|
+
cancelable: true
|
|
448
|
+
})
|
|
449
|
+
container.dispatchEvent(inputEvent)
|
|
450
|
+
|
|
451
|
+
// 方式3: 触发 change 事件
|
|
452
|
+
const changeEvent = new Event('change', {
|
|
453
|
+
bubbles: true,
|
|
454
|
+
cancelable: true
|
|
455
|
+
})
|
|
456
|
+
container.dispatchEvent(changeEvent)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error('插入链接失败:', error)
|
|
461
|
+
// 如果出错,回退到普通粘贴
|
|
462
|
+
setTimeout(() => {
|
|
463
|
+
const textNode = document.createTextNode(pasteText)
|
|
464
|
+
const selection = window.getSelection()
|
|
465
|
+
if (selection && selection.rangeCount > 0) {
|
|
466
|
+
const range = selection.getRangeAt(0)
|
|
467
|
+
range.deleteContents()
|
|
468
|
+
range.insertNode(textNode)
|
|
469
|
+
range.setStartAfter(textNode)
|
|
470
|
+
range.collapse(true)
|
|
471
|
+
selection.removeAllRanges()
|
|
472
|
+
selection.addRange(range)
|
|
473
|
+
}
|
|
474
|
+
}, 0)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return false
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 在捕获阶段监听粘贴事件
|
|
482
|
+
container.addEventListener('paste', handlePaste, true)
|
|
483
|
+
|
|
484
|
+
// 保存处理器引用,以便在组件销毁时移除
|
|
485
|
+
this._pasteHandler = handlePaste
|
|
486
|
+
this._pasteHandlerContainer = container
|
|
487
|
+
},
|
|
488
|
+
// 设置只读模式:允许选择和点击链接,但禁止编辑
|
|
489
|
+
setReadOnlyMode(readOnly) {
|
|
490
|
+
if (!this._editor) return
|
|
491
|
+
|
|
492
|
+
// 使用 wangEditor 的 disable/enable 方法
|
|
493
|
+
if (readOnly) {
|
|
494
|
+
// 禁用编辑器
|
|
495
|
+
if (
|
|
496
|
+
this._editor.disable &&
|
|
497
|
+
typeof this._editor.disable === 'function'
|
|
498
|
+
) {
|
|
499
|
+
this._editor.disable()
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
// 启用编辑器
|
|
503
|
+
if (this._editor.enable && typeof this._editor.enable === 'function') {
|
|
504
|
+
this._editor.enable()
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
this.$nextTick(() => {
|
|
509
|
+
// 查找编辑器的文本容器
|
|
510
|
+
let textContainer = null
|
|
511
|
+
if (this._editor.$textElem && this._editor.$textElem[0]) {
|
|
512
|
+
textContainer = this._editor.$textElem[0]
|
|
513
|
+
} else {
|
|
514
|
+
const editorId = this._editor.id
|
|
515
|
+
if (editorId) {
|
|
516
|
+
const editorEl = document.getElementById(editorId)
|
|
517
|
+
if (editorEl) {
|
|
518
|
+
textContainer = editorEl.querySelector('.w-e-text')
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 也尝试查找容器
|
|
524
|
+
if (!textContainer) {
|
|
525
|
+
if (
|
|
526
|
+
this._editor.$textContainerElem &&
|
|
527
|
+
this._editor.$textContainerElem[0]
|
|
528
|
+
) {
|
|
529
|
+
textContainer = this._editor.$textContainerElem[0]
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (textContainer) {
|
|
534
|
+
if (readOnly) {
|
|
535
|
+
// 强制设置为只读:禁用编辑,但允许选择和点击链接
|
|
536
|
+
const forceReadOnly = () => {
|
|
537
|
+
// 强制设置 contenteditable
|
|
538
|
+
textContainer.setAttribute('contenteditable', 'false')
|
|
539
|
+
// 确保文本可以选择
|
|
540
|
+
textContainer.style.userSelect = 'text'
|
|
541
|
+
textContainer.style.webkitUserSelect = 'text'
|
|
542
|
+
textContainer.style.mozUserSelect = 'text'
|
|
543
|
+
textContainer.style.msUserSelect = 'text'
|
|
544
|
+
textContainer.style.cursor = 'text'
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
forceReadOnly()
|
|
548
|
+
|
|
549
|
+
// 监听 contenteditable 属性的变化,强制保持只读
|
|
550
|
+
const attributeObserver = new MutationObserver(() => {
|
|
551
|
+
if (textContainer.getAttribute('contenteditable') !== 'false') {
|
|
552
|
+
forceReadOnly()
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
attributeObserver.observe(textContainer, {
|
|
556
|
+
attributes: true,
|
|
557
|
+
attributeFilter: ['contenteditable']
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
// 阻止所有编辑操作的事件处理器
|
|
561
|
+
const preventAllEdit = (e) => {
|
|
562
|
+
// 阻止所有输入操作
|
|
563
|
+
e.preventDefault()
|
|
564
|
+
e.stopPropagation()
|
|
565
|
+
e.stopImmediatePropagation()
|
|
566
|
+
return false
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// 阻止键盘输入(但允许选择快捷键如 Ctrl+A, Ctrl+C)
|
|
570
|
+
const preventEdit = (e) => {
|
|
571
|
+
// 允许复制、全选等快捷键
|
|
572
|
+
if (
|
|
573
|
+
(e.ctrlKey || e.metaKey) &&
|
|
574
|
+
(e.key === 'a' || e.key === 'c' || e.key === 'x')
|
|
575
|
+
) {
|
|
576
|
+
return true
|
|
577
|
+
}
|
|
578
|
+
// 阻止所有其他键盘输入
|
|
579
|
+
if (e.key && e.key.length === 1) {
|
|
580
|
+
e.preventDefault()
|
|
581
|
+
e.stopPropagation()
|
|
582
|
+
return false
|
|
583
|
+
}
|
|
584
|
+
// 阻止删除键(当没有选中内容时)
|
|
585
|
+
if (
|
|
586
|
+
(e.key === 'Delete' || e.key === 'Backspace') &&
|
|
587
|
+
!e.ctrlKey &&
|
|
588
|
+
!e.metaKey
|
|
589
|
+
) {
|
|
590
|
+
const selection = window.getSelection()
|
|
591
|
+
if (
|
|
592
|
+
!selection ||
|
|
593
|
+
selection.rangeCount === 0 ||
|
|
594
|
+
selection.isCollapsed
|
|
595
|
+
) {
|
|
596
|
+
e.preventDefault()
|
|
597
|
+
e.stopPropagation()
|
|
598
|
+
return false
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 添加事件监听器(使用捕获阶段,优先级最高)
|
|
604
|
+
textContainer.addEventListener('keydown', preventEdit, true)
|
|
605
|
+
textContainer.addEventListener('keypress', preventAllEdit, true)
|
|
606
|
+
textContainer.addEventListener('paste', preventAllEdit, true)
|
|
607
|
+
textContainer.addEventListener('drop', preventAllEdit, true)
|
|
608
|
+
textContainer.addEventListener('input', preventAllEdit, true)
|
|
609
|
+
textContainer.addEventListener('beforeinput', preventAllEdit, true)
|
|
610
|
+
textContainer.addEventListener(
|
|
611
|
+
'compositionstart',
|
|
612
|
+
preventAllEdit,
|
|
613
|
+
true
|
|
614
|
+
)
|
|
615
|
+
textContainer.addEventListener(
|
|
616
|
+
'compositionupdate',
|
|
617
|
+
preventAllEdit,
|
|
618
|
+
true
|
|
619
|
+
)
|
|
620
|
+
textContainer.addEventListener(
|
|
621
|
+
'compositionend',
|
|
622
|
+
preventAllEdit,
|
|
623
|
+
true
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
// 保存事件处理器引用,以便清理
|
|
627
|
+
if (!textContainer._readOnlyHandlers) {
|
|
628
|
+
textContainer._readOnlyHandlers = {}
|
|
629
|
+
}
|
|
630
|
+
textContainer._readOnlyHandlers.preventEdit = preventEdit
|
|
631
|
+
textContainer._readOnlyHandlers.preventAllEdit = preventAllEdit
|
|
632
|
+
textContainer._readOnlyAttributeObserver = attributeObserver
|
|
633
|
+
|
|
634
|
+
// 允许链接点击
|
|
635
|
+
const enableLinks = () => {
|
|
636
|
+
const links = textContainer.querySelectorAll('a')
|
|
637
|
+
links.forEach((link) => {
|
|
638
|
+
link.style.pointerEvents = 'auto'
|
|
639
|
+
link.style.cursor = 'pointer'
|
|
640
|
+
})
|
|
641
|
+
}
|
|
642
|
+
enableLinks()
|
|
643
|
+
|
|
644
|
+
// 监听新添加的链接
|
|
645
|
+
const linkObserver = new MutationObserver(() => {
|
|
646
|
+
enableLinks()
|
|
647
|
+
// 确保 contenteditable 保持为 false
|
|
648
|
+
forceReadOnly()
|
|
649
|
+
})
|
|
650
|
+
linkObserver.observe(textContainer, {
|
|
651
|
+
childList: true,
|
|
652
|
+
subtree: true,
|
|
653
|
+
attributes: true,
|
|
654
|
+
attributeFilter: ['contenteditable']
|
|
655
|
+
})
|
|
656
|
+
textContainer._readOnlyLinkObserver = linkObserver
|
|
657
|
+
|
|
658
|
+
// 定期检查并强制设置(防止被覆盖)
|
|
659
|
+
if (!textContainer._readOnlyInterval) {
|
|
660
|
+
textContainer._readOnlyInterval = setInterval(() => {
|
|
661
|
+
if (textContainer.getAttribute('contenteditable') !== 'false') {
|
|
662
|
+
forceReadOnly()
|
|
663
|
+
}
|
|
664
|
+
}, 100)
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
667
|
+
// 取消只读:恢复编辑功能
|
|
668
|
+
textContainer.setAttribute('contenteditable', 'true')
|
|
669
|
+
|
|
670
|
+
// 清除定时器
|
|
671
|
+
if (textContainer._readOnlyInterval) {
|
|
672
|
+
clearInterval(textContainer._readOnlyInterval)
|
|
673
|
+
delete textContainer._readOnlyInterval
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// 移除事件监听器
|
|
677
|
+
if (textContainer._readOnlyHandlers) {
|
|
678
|
+
const handlers = textContainer._readOnlyHandlers
|
|
679
|
+
textContainer.removeEventListener(
|
|
680
|
+
'keydown',
|
|
681
|
+
handlers.preventEdit,
|
|
682
|
+
true
|
|
683
|
+
)
|
|
684
|
+
textContainer.removeEventListener(
|
|
685
|
+
'keypress',
|
|
686
|
+
handlers.preventAllEdit,
|
|
687
|
+
true
|
|
688
|
+
)
|
|
689
|
+
textContainer.removeEventListener(
|
|
690
|
+
'paste',
|
|
691
|
+
handlers.preventAllEdit,
|
|
692
|
+
true
|
|
693
|
+
)
|
|
694
|
+
textContainer.removeEventListener(
|
|
695
|
+
'drop',
|
|
696
|
+
handlers.preventAllEdit,
|
|
697
|
+
true
|
|
698
|
+
)
|
|
699
|
+
textContainer.removeEventListener(
|
|
700
|
+
'input',
|
|
701
|
+
handlers.preventAllEdit,
|
|
702
|
+
true
|
|
703
|
+
)
|
|
704
|
+
textContainer.removeEventListener(
|
|
705
|
+
'beforeinput',
|
|
706
|
+
handlers.preventAllEdit,
|
|
707
|
+
true
|
|
708
|
+
)
|
|
709
|
+
textContainer.removeEventListener(
|
|
710
|
+
'compositionstart',
|
|
711
|
+
handlers.preventAllEdit,
|
|
712
|
+
true
|
|
713
|
+
)
|
|
714
|
+
textContainer.removeEventListener(
|
|
715
|
+
'compositionupdate',
|
|
716
|
+
handlers.preventAllEdit,
|
|
717
|
+
true
|
|
718
|
+
)
|
|
719
|
+
textContainer.removeEventListener(
|
|
720
|
+
'compositionend',
|
|
721
|
+
handlers.preventAllEdit,
|
|
722
|
+
true
|
|
723
|
+
)
|
|
724
|
+
delete textContainer._readOnlyHandlers
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// 停止观察
|
|
728
|
+
if (textContainer._readOnlyAttributeObserver) {
|
|
729
|
+
textContainer._readOnlyAttributeObserver.disconnect()
|
|
730
|
+
delete textContainer._readOnlyAttributeObserver
|
|
731
|
+
}
|
|
732
|
+
if (textContainer._readOnlyLinkObserver) {
|
|
733
|
+
textContainer._readOnlyLinkObserver.disconnect()
|
|
734
|
+
delete textContainer._readOnlyLinkObserver
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
})
|
|
739
|
+
},
|
|
740
|
+
// 验证是否为有效的URL
|
|
741
|
+
isValidUrl(str) {
|
|
742
|
+
if (!str || typeof str !== 'string') return false
|
|
743
|
+
|
|
744
|
+
const trimmed = str.trim()
|
|
745
|
+
if (!trimmed) return false
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
// 检查是否包含常见协议
|
|
749
|
+
const urlPattern = /^(https?|ftp|file):\/\//i
|
|
750
|
+
if (urlPattern.test(trimmed)) {
|
|
751
|
+
new URL(trimmed)
|
|
752
|
+
return true
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// 检查是否看起来像URL(包含域名)
|
|
756
|
+
// 匹配形如 www.example.com 或 example.com 的格式
|
|
757
|
+
const domainPattern =
|
|
758
|
+
/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/i
|
|
759
|
+
// 或者简单的 http/https URL 格式
|
|
760
|
+
const simpleUrlPattern = /^[a-zA-Z0-9][a-zA-Z0-9\-]*\.[a-zA-Z]{2,}/
|
|
761
|
+
|
|
762
|
+
if (domainPattern.test(trimmed) || simpleUrlPattern.test(trimmed)) {
|
|
763
|
+
// 尝试添加 http:// 前缀来验证
|
|
764
|
+
try {
|
|
765
|
+
new URL(`http://${trimmed}`)
|
|
766
|
+
return true
|
|
767
|
+
} catch (e) {
|
|
768
|
+
return false
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return false
|
|
773
|
+
} catch (e) {
|
|
774
|
+
return false
|
|
775
|
+
}
|
|
160
776
|
},
|
|
161
777
|
handleDefaultResponse(res, insertImgFn) {
|
|
162
778
|
// 处理标准响应格式: {errno: 0, data: {url: "...", alt: "...", href: "..."}}
|
package/src/core/alias.js
CHANGED