@bagelink/vue 0.0.1025 → 0.0.1031
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 +1 -0
- package/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/ToolBar.vue.d.ts +3 -0
- package/dist/components/ToolBar.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/NumberInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RangeInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/RangeInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +12 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts +9 -0
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +40 -20
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts +1 -0
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts +17 -0
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/debug.d.ts +48 -0
- package/dist/components/form/inputs/RichText/utils/debug.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts +3 -1
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +13 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +4 -0
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/SignaturePad.vue.d.ts +2 -2
- package/dist/components/form/inputs/SignaturePad.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/UploadFile.vue.d.ts +86 -0
- package/dist/components/form/inputs/Upload/UploadFile.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/Upload/upload.d.ts +13 -0
- package/dist/components/form/inputs/Upload/upload.d.ts.map +1 -0
- package/dist/components/form/inputs/index.d.ts +1 -0
- package/dist/components/form/inputs/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/editor-B3mMCQSg.cjs +4 -0
- package/dist/editor-BKPRpAjr.js +4 -0
- package/dist/index.cjs +10059 -6970
- package/dist/index.mjs +10065 -6976
- package/dist/plugins/bagel.d.ts +1 -0
- package/dist/plugins/bagel.d.ts.map +1 -1
- package/dist/style.css +666 -458
- package/package.json +1 -1
- package/src/components/Image.vue +1 -2
- package/src/components/ToolBar.vue +9 -0
- package/src/components/form/inputs/NumberInput.vue +8 -1
- package/src/components/form/inputs/RangeInput.vue +6 -5
- package/src/components/form/inputs/RichText/composables/useCommands.ts +114 -0
- package/src/components/form/inputs/RichText/composables/useEditor.ts +257 -129
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +64 -19
- package/src/components/form/inputs/RichText/config.ts +18 -2
- package/src/components/form/inputs/RichText/editor.css +17 -15
- package/src/components/form/inputs/RichText/index.vue +67 -13
- package/src/components/form/inputs/RichText/richTextTypes.d.ts +2 -0
- package/src/components/form/inputs/RichText/utils/commands.ts +37 -0
- package/src/components/form/inputs/RichText/utils/debug.ts +196 -0
- package/src/components/form/inputs/RichText/utils/formatting.ts +168 -288
- package/src/components/form/inputs/RichText/utils/selection.ts +77 -0
- package/src/components/form/inputs/RichText/utils/table.ts +66 -0
- package/src/components/form/inputs/SignaturePad.vue +2 -2
- package/src/components/form/inputs/Upload/UploadFile.vue +357 -0
- package/src/components/form/inputs/Upload/upload.css +232 -0
- package/src/components/form/inputs/Upload/upload.ts +22 -0
- package/src/components/form/inputs/Upload/upload.types.d.ts +43 -0
- package/src/components/form/inputs/index.ts +1 -0
- package/src/components/index.ts +2 -0
- package/src/plugins/bagel.ts +2 -2
- /package/src/components/form/inputs/RichText/components/{Toolbar.vue → EditorToolbar.vue} +0 -0
package/package.json
CHANGED
package/src/components/Image.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { Skeleton,
|
|
2
|
+
import { Skeleton, normalizeDimension, appendScript } from '@bagelink/vue'
|
|
3
3
|
import { watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
declare global {
|
|
@@ -23,7 +23,6 @@ async function loadImage(src: string) {
|
|
|
23
23
|
imageSrc = null
|
|
24
24
|
return
|
|
25
25
|
}
|
|
26
|
-
src = normalizeURL(src)
|
|
27
26
|
const ext = src.split('.').pop()?.toLowerCase().split('?').shift()
|
|
28
27
|
|
|
29
28
|
if (ext === 'heic') {
|
|
@@ -104,7 +104,11 @@ watch(() => modelValue, (newVal) => {
|
|
|
104
104
|
v-model.trim="formattedValue"
|
|
105
105
|
v-pattern.number
|
|
106
106
|
type="text"
|
|
107
|
-
:class="{
|
|
107
|
+
:class="{
|
|
108
|
+
'txt-center': center,
|
|
109
|
+
'min0': layout,
|
|
110
|
+
'bgl-number-input': layout !== 'vertical' && layout !== 'horizontal',
|
|
111
|
+
}"
|
|
108
112
|
:placeholder="placeholder || label"
|
|
109
113
|
:disabled
|
|
110
114
|
:required
|
|
@@ -160,4 +164,7 @@ watch(() => modelValue, (newVal) => {
|
|
|
160
164
|
.bgl-big-ctrl-num-btn{
|
|
161
165
|
width: 100% !important;
|
|
162
166
|
}
|
|
167
|
+
.bgl-number-input{
|
|
168
|
+
padding-inline-end: 1.75rem !important;
|
|
169
|
+
}
|
|
163
170
|
</style>
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { watch } from 'vue'
|
|
3
3
|
|
|
4
4
|
interface props {
|
|
5
|
-
min: number
|
|
6
|
-
max: number
|
|
7
5
|
modelValue: number | [number, number]
|
|
6
|
+
min?: number
|
|
7
|
+
max?: number
|
|
8
8
|
step?: number
|
|
9
9
|
required?: boolean
|
|
10
10
|
label?: string
|
|
@@ -41,7 +41,8 @@ const toPercentage = $computed(() => ((validTo - min) / (max - min)) * 100)
|
|
|
41
41
|
|
|
42
42
|
const rangeStyle = $computed(() => {
|
|
43
43
|
const width = ((validTo - validFrom) / (max - min)) * 100
|
|
44
|
-
if (
|
|
44
|
+
if (isRange) return { left: `${fromPercentage}%`, width: `${width}%` }
|
|
45
|
+
if (rtl) return { left: `${width}%`, width: `${fromPercentage}%` }
|
|
45
46
|
return { right: `${width}%`, width: `${fromPercentage}%` }
|
|
46
47
|
})
|
|
47
48
|
</script>
|
|
@@ -49,7 +50,7 @@ const rangeStyle = $computed(() => {
|
|
|
49
50
|
<template>
|
|
50
51
|
<div>
|
|
51
52
|
<label class="label">{{ label }}</label>
|
|
52
|
-
<div class="range-slider relative w-100">
|
|
53
|
+
<div class="range-slider relative w-100" :dir="rtl ? 'rtl' : 'ltr'">
|
|
53
54
|
<input
|
|
54
55
|
:id="id"
|
|
55
56
|
v-model="from"
|
|
@@ -84,7 +85,7 @@ const rangeStyle = $computed(() => {
|
|
|
84
85
|
</div>
|
|
85
86
|
<p
|
|
86
87
|
v-if="from !== min"
|
|
87
|
-
class="txt-center txt-12 range-slider-position-txt absolute line-height-1 opacity-0
|
|
88
|
+
class="txt-center txt-12 range-slider-position-txt absolute line-height-1 opacity-0"
|
|
88
89
|
:style="{ '--progress': `${fromPercentage}` }"
|
|
89
90
|
>
|
|
90
91
|
<span>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { EditorState } from '../richTextTypes'
|
|
2
|
+
import { type CommandRegistry, createCommandExecutor } from '../utils/commands'
|
|
3
|
+
import { formatting } from '../utils/formatting'
|
|
4
|
+
import { insertImage, insertLink } from '../utils/media'
|
|
5
|
+
import { addRow, deleteRow, mergeCells, splitCell, insertTable, deleteTable, insertColumn, deleteColumn, alignColumn } from '../utils/table'
|
|
6
|
+
|
|
7
|
+
export function useCommands(state: EditorState, debug?: { logCommand: (command: string, value?: string) => void }) {
|
|
8
|
+
const format = () => formatting(state)
|
|
9
|
+
|
|
10
|
+
const commands: CommandRegistry = {
|
|
11
|
+
// Text formatting commands
|
|
12
|
+
bold: {
|
|
13
|
+
name: 'Bold',
|
|
14
|
+
execute: (state: EditorState) => { format().text('bold') },
|
|
15
|
+
isActive: (state: EditorState) => state.selectedStyles.has('bold')
|
|
16
|
+
},
|
|
17
|
+
italic: {
|
|
18
|
+
name: 'Italic',
|
|
19
|
+
execute: (state: EditorState) => { format().text('italic') },
|
|
20
|
+
isActive: (state: EditorState) => state.selectedStyles.has('italic')
|
|
21
|
+
},
|
|
22
|
+
underline: {
|
|
23
|
+
name: 'Underline',
|
|
24
|
+
execute: (state: EditorState) => { format().text('underline') },
|
|
25
|
+
isActive: (state: EditorState) => state.selectedStyles.has('underline')
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// Block formatting commands
|
|
29
|
+
h1: { name: 'Heading 1', execute: (state: EditorState) => { format().block('h1', 'h1') }, isActive: (state: EditorState) => state.selectedStyles.has('h1') },
|
|
30
|
+
h2: { name: 'Heading 2', execute: (state: EditorState) => { format().block('h2', 'h2') }, isActive: (state: EditorState) => state.selectedStyles.has('h2') },
|
|
31
|
+
h3: { name: 'Heading 3', execute: (state: EditorState) => { format().block('h3', 'h3') }, isActive: (state: EditorState) => state.selectedStyles.has('h3') },
|
|
32
|
+
h4: { name: 'Heading 4', execute: (state: EditorState) => { format().block('h4', 'h4') }, isActive: (state: EditorState) => state.selectedStyles.has('h4') },
|
|
33
|
+
h5: { name: 'Heading 5', execute: (state: EditorState) => { format().block('h5', 'h5') }, isActive: (state: EditorState) => state.selectedStyles.has('h5') },
|
|
34
|
+
h6: { name: 'Heading 6', execute: (state: EditorState) => { format().block('h6', 'h6') }, isActive: (state: EditorState) => state.selectedStyles.has('h6') },
|
|
35
|
+
p: { name: 'Paragraph', execute: (state: EditorState) => { format().block('p', 'p') }, isActive: (state: EditorState) => state.selectedStyles.has('p') },
|
|
36
|
+
blockquote: {
|
|
37
|
+
name: 'Blockquote',
|
|
38
|
+
execute: (state: EditorState) => { format().block('blockquote', 'blockquote') },
|
|
39
|
+
isActive: (state: EditorState) => state.selectedStyles.has('blockquote')
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// List commands
|
|
43
|
+
orderedList: {
|
|
44
|
+
name: 'Ordered List',
|
|
45
|
+
execute: (state: EditorState) => { format().list('orderedList') },
|
|
46
|
+
isActive: (state: EditorState) => state.selectedStyles.has('orderedList')
|
|
47
|
+
},
|
|
48
|
+
unorderedList: {
|
|
49
|
+
name: 'Unordered List',
|
|
50
|
+
execute: (state: EditorState) => { format().list('unorderedList') },
|
|
51
|
+
isActive: (state: EditorState) => state.selectedStyles.has('unorderedList')
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Table commands
|
|
55
|
+
insertTable: {
|
|
56
|
+
name: 'Insert Table',
|
|
57
|
+
execute: (state: EditorState, value?: string) => {
|
|
58
|
+
const [rows, cols] = value?.split('x').map(Number) || [3, 3]
|
|
59
|
+
insertTable(rows, cols, state)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
deleteTable: { name: 'Delete Table', execute: (state: EditorState) => state.range && deleteTable(state.range) },
|
|
63
|
+
mergeCells: { name: 'Merge Cells', execute: (state: EditorState) => state.range && state.doc && mergeCells(state.range, state.doc) },
|
|
64
|
+
splitCells: { name: 'Split Cells', execute: (state: EditorState) => state.range && state.doc && splitCell(state.range, state.doc) },
|
|
65
|
+
addRowBefore: { name: 'Add Row Before', execute: (state: EditorState) => state.range && state.doc && addRow('before', state.range, state.doc) },
|
|
66
|
+
addRowAfter: { name: 'Add Row After', execute: (state: EditorState) => state.range && state.doc && addRow('after', state.range, state.doc) },
|
|
67
|
+
deleteRow: { name: 'Delete Row', execute: (state: EditorState) => state.range && deleteRow(state.range) },
|
|
68
|
+
insertColumnLeft: { name: 'Insert Column Left', execute: (state: EditorState) => state.range && insertColumn('before', state.range) },
|
|
69
|
+
insertColumnRight: { name: 'Insert Column Right', execute: (state: EditorState) => state.range && insertColumn('after', state.range) },
|
|
70
|
+
deleteColumn: { name: 'Delete Column', execute: (state: EditorState) => state.range && deleteColumn(state.range) },
|
|
71
|
+
|
|
72
|
+
// Alignment commands
|
|
73
|
+
alignLeft: { name: 'Align Left', execute: (state: EditorState) => state.range && alignColumn(state.range, 'left') },
|
|
74
|
+
alignCenter: { name: 'Align Center', execute: (state: EditorState) => state.range && alignColumn(state.range, 'center') },
|
|
75
|
+
alignRight: { name: 'Align Right', execute: (state: EditorState) => state.range && alignColumn(state.range, 'right') },
|
|
76
|
+
alignJustify: { name: 'Align Justify', execute: (state: EditorState) => state.range && alignColumn(state.range, 'justify') },
|
|
77
|
+
|
|
78
|
+
// Media commands
|
|
79
|
+
image: { name: 'Insert Image', execute: (state: EditorState) => state.modal && insertImage(state.modal, state) },
|
|
80
|
+
link: { name: 'Insert Link', execute: (state: EditorState) => state.modal && state.range && insertLink(state.modal, state) },
|
|
81
|
+
|
|
82
|
+
// Other commands
|
|
83
|
+
clear: { name: 'Clear Formatting', execute: (state: EditorState) => { format().clear() } },
|
|
84
|
+
indent: { name: 'Indent', execute: (state: EditorState) => { format().text('indent') } },
|
|
85
|
+
outdent: { name: 'Outdent', execute: (state: EditorState) => { format().text('outdent') } },
|
|
86
|
+
fullScreen: {
|
|
87
|
+
name: 'Full Screen',
|
|
88
|
+
execute: (state: EditorState) => { state.isFullscreen = !state.isFullscreen },
|
|
89
|
+
isActive: (state: EditorState) => state.isFullscreen
|
|
90
|
+
},
|
|
91
|
+
splitView: {
|
|
92
|
+
name: 'Split View',
|
|
93
|
+
execute: (state: EditorState) => { state.isSplitView = !state.isSplitView },
|
|
94
|
+
isActive: (state: EditorState) => state.isSplitView
|
|
95
|
+
},
|
|
96
|
+
codeView: {
|
|
97
|
+
name: 'Code View',
|
|
98
|
+
execute: (state: EditorState) => { state.isCodeView = !state.isCodeView },
|
|
99
|
+
isActive: (state: EditorState) => state.isCodeView
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const executor = createCommandExecutor(state, commands)
|
|
104
|
+
|
|
105
|
+
// Wrap executor with debug logging if available
|
|
106
|
+
return {
|
|
107
|
+
execute: (command: string, value?: string) => {
|
|
108
|
+
debug?.logCommand(command, value)
|
|
109
|
+
executor.execute(command, value)
|
|
110
|
+
},
|
|
111
|
+
isActive: executor.isActive,
|
|
112
|
+
getValue: executor.getValue
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -1,41 +1,47 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { EditorDebugger } from '../utils/debug'
|
|
2
|
+
import { useModal } from '@bagelink/vue'
|
|
3
|
+
import { reactive, ref } from 'vue'
|
|
3
4
|
import { formatting } from '../utils/formatting'
|
|
4
|
-
import { insertImage, insertLink } from '../utils/media'
|
|
5
5
|
import { isStyleActive } from '../utils/selection'
|
|
6
|
-
import { addRow, deleteRow, mergeCells, splitCell, insertTable } from '../utils/table'
|
|
6
|
+
import { addRow, deleteRow, mergeCells, splitCell, insertTable, deleteTable, insertColumn, deleteColumn, alignColumn } from '../utils/table'
|
|
7
|
+
|
|
8
|
+
interface EditorState {
|
|
9
|
+
content: string
|
|
10
|
+
doc: Document | undefined
|
|
11
|
+
selection: Selection | null
|
|
12
|
+
selectedStyles: Set<string>
|
|
13
|
+
isFullscreen: boolean
|
|
14
|
+
isSplitView: boolean
|
|
15
|
+
isCodeView: boolean
|
|
16
|
+
hasInit: boolean
|
|
17
|
+
undoStack: string[]
|
|
18
|
+
redoStack: string[]
|
|
19
|
+
rangeCount: number
|
|
20
|
+
range: Range | null
|
|
21
|
+
modal: ReturnType<typeof useModal>
|
|
22
|
+
}
|
|
7
23
|
|
|
8
24
|
export function useEditor() {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
const editorDebugger = ref<EditorDebugger>()
|
|
26
|
+
const modal = useModal()
|
|
27
|
+
|
|
28
|
+
const state = reactive<EditorState>({
|
|
29
|
+
content: '',
|
|
30
|
+
doc: undefined,
|
|
31
|
+
selection: null,
|
|
32
|
+
selectedStyles: new Set(),
|
|
12
33
|
isFullscreen: false,
|
|
13
|
-
hasInit: false,
|
|
14
|
-
isCodeView: false,
|
|
15
34
|
isSplitView: false,
|
|
16
|
-
|
|
35
|
+
isCodeView: false,
|
|
36
|
+
hasInit: false,
|
|
37
|
+
undoStack: [],
|
|
38
|
+
redoStack: [],
|
|
17
39
|
rangeCount: 0,
|
|
18
|
-
|
|
40
|
+
range: null,
|
|
41
|
+
modal
|
|
19
42
|
})
|
|
20
43
|
|
|
21
|
-
|
|
22
|
-
if (state.selection?.rangeCount) {
|
|
23
|
-
const node = state.selection.getRangeAt(0).commonAncestorContainer
|
|
24
|
-
const parentElement = node.nodeType === 3 ? node.parentElement : node
|
|
25
|
-
if (parentElement) {
|
|
26
|
-
const list = (parentElement as Element).closest('ul, ol')
|
|
27
|
-
if (list) {
|
|
28
|
-
styles.add(
|
|
29
|
-
list.tagName.toLowerCase() === 'ul'
|
|
30
|
-
? 'unorderedList'
|
|
31
|
-
: 'orderedList'
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const updateActiveStyles = () => {
|
|
44
|
+
function updateActiveStyles() {
|
|
39
45
|
if (!state.doc) return
|
|
40
46
|
const styles = new Set<string>()
|
|
41
47
|
const styleTypes = [
|
|
@@ -60,11 +66,26 @@ export function useEditor() {
|
|
|
60
66
|
styles.add(style)
|
|
61
67
|
}
|
|
62
68
|
})
|
|
63
|
-
updateListStyles(styles)
|
|
64
69
|
state.selectedStyles = styles
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
function updateContent(source: 'html' | 'text') {
|
|
73
|
+
if (!state.doc) return
|
|
74
|
+
|
|
75
|
+
// Save current state for undo
|
|
76
|
+
state.undoStack.push(state.content)
|
|
77
|
+
// Clear redo stack on new content
|
|
78
|
+
state.redoStack = []
|
|
79
|
+
|
|
80
|
+
if (source === 'html') {
|
|
81
|
+
state.doc.body.innerHTML = state.content
|
|
82
|
+
} else {
|
|
83
|
+
state.doc.body.textContent = state.content
|
|
84
|
+
}
|
|
85
|
+
updateActiveStyles()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function updateSelection() {
|
|
68
89
|
if (!state.doc) return
|
|
69
90
|
state.selection = state.doc.getSelection()
|
|
70
91
|
if (!state.selection) return
|
|
@@ -93,20 +114,132 @@ export function useEditor() {
|
|
|
93
114
|
}
|
|
94
115
|
}
|
|
95
116
|
|
|
96
|
-
|
|
117
|
+
function setupEventListeners(doc: Document) {
|
|
118
|
+
// Input and selection events
|
|
119
|
+
doc.addEventListener('input', () => {
|
|
120
|
+
state.content = doc.body.innerHTML
|
|
121
|
+
updateActiveStyles()
|
|
122
|
+
})
|
|
97
123
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
124
|
+
doc.addEventListener('selectionchange', () => {
|
|
125
|
+
updateSelection()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
doc.addEventListener('mouseup', () => {
|
|
129
|
+
updateSelection()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
doc.addEventListener('keyup', (e) => {
|
|
133
|
+
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
|
134
|
+
updateSelection()
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Clean empty tags and normalize content
|
|
139
|
+
const cleanEmptyTags = () => {
|
|
140
|
+
const walker = doc.createTreeWalker(
|
|
141
|
+
doc.body,
|
|
142
|
+
NodeFilter.SHOW_ELEMENT,
|
|
143
|
+
null
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const nodesToRemove: Element[] = []
|
|
147
|
+
let node = walker.nextNode() as Element
|
|
148
|
+
|
|
149
|
+
while (node) {
|
|
150
|
+
// Skip certain elements
|
|
151
|
+
if (['br', 'img', 'hr', 'input'].includes(node.tagName.toLowerCase())) {
|
|
152
|
+
node = walker.nextNode() as Element
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Get text content without extra spaces
|
|
157
|
+
const textContent = node.textContent?.trim() || ''
|
|
158
|
+
const innerHTML = node.innerHTML.trim()
|
|
159
|
+
const hasOnlyBr = innerHTML === '<br>' || innerHTML === '<br/>'
|
|
160
|
+
const hasOnlyNbsp = innerHTML === ' ' || textContent === '\u00A0'
|
|
161
|
+
const isEmpty = !textContent && !innerHTML
|
|
162
|
+
const isDirectChildOfBody = node.parentElement === doc.body
|
|
163
|
+
|
|
164
|
+
// Handle empty or unnecessary tags
|
|
165
|
+
if (isEmpty || hasOnlyNbsp || (hasOnlyBr && !isDirectChildOfBody)) {
|
|
166
|
+
// If it's a direct child of body, replace with a proper paragraph
|
|
167
|
+
if (isDirectChildOfBody) {
|
|
168
|
+
if (!node.matches('p')) {
|
|
169
|
+
const p = doc.createElement('p')
|
|
170
|
+
p.innerHTML = '<br>'
|
|
171
|
+
node.parentNode?.replaceChild(p, node)
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
nodesToRemove.push(node)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
node = walker.nextNode() as Element
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Remove all marked nodes
|
|
182
|
+
nodesToRemove.forEach((node) => { node.remove() })
|
|
104
183
|
}
|
|
105
|
-
|
|
184
|
+
|
|
185
|
+
// Regular cleanup
|
|
186
|
+
const observer = new MutationObserver(() => {
|
|
187
|
+
cleanEmptyTags()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
observer.observe(doc.body, {
|
|
191
|
+
childList: true,
|
|
192
|
+
subtree: true,
|
|
193
|
+
characterData: true
|
|
194
|
+
})
|
|
106
195
|
}
|
|
107
196
|
|
|
108
|
-
|
|
197
|
+
function init(doc: Document) {
|
|
198
|
+
state.doc = doc
|
|
199
|
+
state.hasInit = true
|
|
200
|
+
setupEventListeners(doc)
|
|
201
|
+
updateContent('html')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function handleUndo() {
|
|
205
|
+
if (state.undoStack.length === 0) return
|
|
206
|
+
|
|
207
|
+
// Save current state to redo stack
|
|
208
|
+
state.redoStack.push(state.content)
|
|
209
|
+
|
|
210
|
+
// Pop and apply last state from undo stack
|
|
211
|
+
const lastContent = state.undoStack.pop()
|
|
212
|
+
if (lastContent !== undefined) {
|
|
213
|
+
state.content = lastContent
|
|
214
|
+
updateContent('html')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function handleRedo() {
|
|
219
|
+
if (state.redoStack.length === 0) return
|
|
220
|
+
|
|
221
|
+
// Save current state to undo stack
|
|
222
|
+
state.undoStack.push(state.content)
|
|
223
|
+
|
|
224
|
+
// Pop and apply last state from redo stack
|
|
225
|
+
const nextContent = state.redoStack.pop()
|
|
226
|
+
if (nextContent !== undefined) {
|
|
227
|
+
state.content = nextContent
|
|
228
|
+
updateContent('html')
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function handleToolbarAction(action: string, value?: string) {
|
|
109
233
|
if (!state.doc) return
|
|
234
|
+
|
|
235
|
+
if (action === 'undo') {
|
|
236
|
+
handleUndo()
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (action === 'redo') {
|
|
240
|
+
handleRedo()
|
|
241
|
+
return
|
|
242
|
+
}
|
|
110
243
|
if (action === 'fullScreen') {
|
|
111
244
|
state.isFullscreen = !state.isFullscreen
|
|
112
245
|
return
|
|
@@ -120,108 +253,103 @@ export function useEditor() {
|
|
|
120
253
|
return
|
|
121
254
|
}
|
|
122
255
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Check if there is a selection or just a caret position
|
|
128
|
-
const isCaret = selection.isCollapsed
|
|
256
|
+
// Apply formatting based on action
|
|
257
|
+
const format = formatting(state)
|
|
258
|
+
state.doc.body.focus()
|
|
129
259
|
|
|
130
|
-
|
|
131
|
-
|
|
260
|
+
switch (action) {
|
|
261
|
+
case 'bold':
|
|
262
|
+
case 'italic':
|
|
263
|
+
case 'underline':
|
|
264
|
+
format.text(action)
|
|
265
|
+
break
|
|
266
|
+
case 'orderedList':
|
|
267
|
+
format.list('ol')
|
|
268
|
+
break
|
|
269
|
+
case 'unorderedList':
|
|
270
|
+
format.list('ul')
|
|
271
|
+
break
|
|
272
|
+
case 'blockquote':
|
|
273
|
+
case 'p':
|
|
274
|
+
case 'h1':
|
|
275
|
+
case 'h2':
|
|
276
|
+
case 'h3':
|
|
277
|
+
case 'h4':
|
|
278
|
+
case 'h5':
|
|
279
|
+
case 'h6':
|
|
280
|
+
format.block(action, action)
|
|
281
|
+
break
|
|
282
|
+
case 'insertTable': {
|
|
283
|
+
const [rows, cols] = value?.split('x').map(Number) || [3, 3]
|
|
284
|
+
insertTable(rows, cols, state)
|
|
285
|
+
break
|
|
132
286
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
287
|
+
case 'deleteTable':
|
|
288
|
+
if (state.range) deleteTable(state.range)
|
|
289
|
+
break
|
|
290
|
+
case 'mergeCells':
|
|
291
|
+
if (state.range && state.doc) mergeCells(state.range, state.doc)
|
|
292
|
+
break
|
|
293
|
+
case 'splitCells':
|
|
294
|
+
if (state.range && state.doc) splitCell(state.range, state.doc)
|
|
295
|
+
break
|
|
296
|
+
case 'addRowBefore':
|
|
297
|
+
case 'addRowAfter':
|
|
298
|
+
if (state.range && state.doc) {
|
|
299
|
+
addRow(action === 'addRowBefore' ? 'before' : 'after', state.range, state.doc)
|
|
139
300
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
case 'addRowBefore':
|
|
149
|
-
case 'addRowAfter':
|
|
150
|
-
if (isCaret) return
|
|
151
|
-
addRow(
|
|
152
|
-
command === 'addRowBefore' ? 'before' : 'after',
|
|
153
|
-
range,
|
|
154
|
-
state.doc
|
|
155
|
-
)
|
|
156
|
-
break
|
|
157
|
-
case 'deleteRow':
|
|
158
|
-
if (isCaret) return
|
|
159
|
-
deleteRow(range)
|
|
160
|
-
break
|
|
161
|
-
case 'bold':
|
|
162
|
-
case 'italic':
|
|
163
|
-
case 'underline':
|
|
164
|
-
format().text(command)
|
|
165
|
-
break
|
|
166
|
-
case 'clear':{
|
|
167
|
-
format().clear()
|
|
168
|
-
break
|
|
301
|
+
break
|
|
302
|
+
case 'deleteRow':
|
|
303
|
+
if (state.range) deleteRow(state.range)
|
|
304
|
+
break
|
|
305
|
+
case 'insertColumnLeft':
|
|
306
|
+
case 'insertColumnRight':
|
|
307
|
+
if (state.range) {
|
|
308
|
+
insertColumn(action === 'insertColumnLeft' ? 'before' : 'after', state.range)
|
|
169
309
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
case 'p':
|
|
188
|
-
format().block(command, value || command)
|
|
189
|
-
break
|
|
190
|
-
default:
|
|
191
|
-
if (/^h[1-6]$/.test(command)) {
|
|
192
|
-
format().block(command, value || command)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
310
|
+
break
|
|
311
|
+
case 'deleteColumn':
|
|
312
|
+
if (state.range) deleteColumn(state.range)
|
|
313
|
+
break
|
|
314
|
+
case 'alignLeft':
|
|
315
|
+
case 'alignCenter':
|
|
316
|
+
case 'alignRight':
|
|
317
|
+
case 'alignJustify':
|
|
318
|
+
if (state.range) {
|
|
319
|
+
alignColumn(state.range, action.replace('align', '').toLowerCase() as 'left' | 'center' | 'right' | 'justify')
|
|
320
|
+
}
|
|
321
|
+
break
|
|
322
|
+
case 'clear':
|
|
323
|
+
format.clear()
|
|
324
|
+
break
|
|
325
|
+
default:
|
|
326
|
+
format.text(action)
|
|
195
327
|
}
|
|
196
|
-
state.doc.body.focus()
|
|
197
|
-
applyFormatting(action, value)
|
|
198
|
-
updateContent()
|
|
199
|
-
updateActiveStyles()
|
|
200
|
-
}
|
|
201
|
-
const setupEventListeners = () => {
|
|
202
|
-
if (!state.doc) return
|
|
203
|
-
state.doc.addEventListener('selectionchange', () => { updateSelection() })
|
|
204
|
-
state.doc.addEventListener('input', () => { updateContent() })
|
|
205
|
-
state.doc.addEventListener('mouseup', () => { updateSelection() })
|
|
206
|
-
state.doc.addEventListener('keyup', (e) => {
|
|
207
|
-
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
|
208
|
-
updateSelection()
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
328
|
|
|
213
|
-
const init = (doc: Document, modal: Modal) => {
|
|
214
|
-
state.doc = doc
|
|
215
|
-
state.modal = modal
|
|
216
|
-
setupEventListeners()
|
|
217
329
|
updateContent('html')
|
|
218
|
-
state.hasInit = true
|
|
219
330
|
}
|
|
220
331
|
|
|
332
|
+
// Debug methods
|
|
333
|
+
const getDebugSession = () => editorDebugger.value?.getSession()
|
|
334
|
+
const clearDebugSession = () => editorDebugger.value?.clearSession()
|
|
335
|
+
const downloadDebugSession = () => editorDebugger.value?.downloadSession()
|
|
336
|
+
const logCommand = (command: string, value?: string) => editorDebugger.value?.logCommand(command, value, state)
|
|
337
|
+
const exportDebugWithPrompt = (message?: string) => editorDebugger.value?.exportSessionWithPrompt(message)
|
|
338
|
+
|
|
221
339
|
return {
|
|
222
340
|
state,
|
|
223
341
|
init,
|
|
224
342
|
handleToolbarAction,
|
|
225
343
|
updateContent,
|
|
344
|
+
handleUndo,
|
|
345
|
+
handleRedo,
|
|
346
|
+
// Debug methods
|
|
347
|
+
debug: {
|
|
348
|
+
getSession: getDebugSession,
|
|
349
|
+
clearSession: clearDebugSession,
|
|
350
|
+
downloadSession: downloadDebugSession,
|
|
351
|
+
logCommand,
|
|
352
|
+
exportDebugWithPrompt
|
|
353
|
+
}
|
|
226
354
|
}
|
|
227
355
|
}
|