@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.
Files changed (70) hide show
  1. package/README.md +1 -0
  2. package/dist/components/Image.vue.d.ts.map +1 -1
  3. package/dist/components/ToolBar.vue.d.ts +3 -0
  4. package/dist/components/ToolBar.vue.d.ts.map +1 -0
  5. package/dist/components/form/inputs/NumberInput.vue.d.ts.map +1 -1
  6. package/dist/components/form/inputs/RangeInput.vue.d.ts +2 -2
  7. package/dist/components/form/inputs/RangeInput.vue.d.ts.map +1 -1
  8. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +12 -0
  9. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -0
  10. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts +9 -0
  11. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -0
  12. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +40 -20
  13. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  14. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  15. package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
  16. package/dist/components/form/inputs/RichText/index.vue.d.ts +1 -0
  17. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  18. package/dist/components/form/inputs/RichText/utils/commands.d.ts +17 -0
  19. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -0
  20. package/dist/components/form/inputs/RichText/utils/debug.d.ts +48 -0
  21. package/dist/components/form/inputs/RichText/utils/debug.d.ts.map +1 -0
  22. package/dist/components/form/inputs/RichText/utils/formatting.d.ts +3 -1
  23. package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -1
  24. package/dist/components/form/inputs/RichText/utils/selection.d.ts +13 -0
  25. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  26. package/dist/components/form/inputs/RichText/utils/table.d.ts +4 -0
  27. package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
  28. package/dist/components/form/inputs/SignaturePad.vue.d.ts +2 -2
  29. package/dist/components/form/inputs/SignaturePad.vue.d.ts.map +1 -1
  30. package/dist/components/form/inputs/Upload/UploadFile.vue.d.ts +86 -0
  31. package/dist/components/form/inputs/Upload/UploadFile.vue.d.ts.map +1 -0
  32. package/dist/components/form/inputs/Upload/upload.d.ts +13 -0
  33. package/dist/components/form/inputs/Upload/upload.d.ts.map +1 -0
  34. package/dist/components/form/inputs/index.d.ts +1 -0
  35. package/dist/components/form/inputs/index.d.ts.map +1 -1
  36. package/dist/components/index.d.ts +1 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/editor-B3mMCQSg.cjs +4 -0
  39. package/dist/editor-BKPRpAjr.js +4 -0
  40. package/dist/index.cjs +10059 -6970
  41. package/dist/index.mjs +10065 -6976
  42. package/dist/plugins/bagel.d.ts +1 -0
  43. package/dist/plugins/bagel.d.ts.map +1 -1
  44. package/dist/style.css +666 -458
  45. package/package.json +1 -1
  46. package/src/components/Image.vue +1 -2
  47. package/src/components/ToolBar.vue +9 -0
  48. package/src/components/form/inputs/NumberInput.vue +8 -1
  49. package/src/components/form/inputs/RangeInput.vue +6 -5
  50. package/src/components/form/inputs/RichText/composables/useCommands.ts +114 -0
  51. package/src/components/form/inputs/RichText/composables/useEditor.ts +257 -129
  52. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +64 -19
  53. package/src/components/form/inputs/RichText/config.ts +18 -2
  54. package/src/components/form/inputs/RichText/editor.css +17 -15
  55. package/src/components/form/inputs/RichText/index.vue +67 -13
  56. package/src/components/form/inputs/RichText/richTextTypes.d.ts +2 -0
  57. package/src/components/form/inputs/RichText/utils/commands.ts +37 -0
  58. package/src/components/form/inputs/RichText/utils/debug.ts +196 -0
  59. package/src/components/form/inputs/RichText/utils/formatting.ts +168 -288
  60. package/src/components/form/inputs/RichText/utils/selection.ts +77 -0
  61. package/src/components/form/inputs/RichText/utils/table.ts +66 -0
  62. package/src/components/form/inputs/SignaturePad.vue +2 -2
  63. package/src/components/form/inputs/Upload/UploadFile.vue +357 -0
  64. package/src/components/form/inputs/Upload/upload.css +232 -0
  65. package/src/components/form/inputs/Upload/upload.ts +22 -0
  66. package/src/components/form/inputs/Upload/upload.types.d.ts +43 -0
  67. package/src/components/form/inputs/index.ts +1 -0
  68. package/src/components/index.ts +2 -0
  69. package/src/plugins/bagel.ts +2 -2
  70. /package/src/components/form/inputs/RichText/components/{Toolbar.vue → EditorToolbar.vue} +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "0.0.1025",
4
+ "version": "0.0.1031",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Neveh Allon",
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { Skeleton, normalizeURL, normalizeDimension, appendScript } from '@bagelink/vue'
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') {
@@ -0,0 +1,9 @@
1
+ <script lang="ts" setup>
2
+
3
+ </script>
4
+
5
+ <template>
6
+ <div class="bg-red">
7
+ 123
8
+ </div>
9
+ </template>
@@ -104,7 +104,11 @@ watch(() => modelValue, (newVal) => {
104
104
  v-model.trim="formattedValue"
105
105
  v-pattern.number
106
106
  type="text"
107
- :class="{ 'txt-center': center, 'min0': layout }"
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 (rtl || isRange) return { left: `${fromPercentage}%`, width: `${width}%` }
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 { EditorState, Modal } from '../richTextTypes'
2
- import { reactive } from 'vue'
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 state = reactive<
10
- EditorState
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
- selectedStyles: new Set<string>(),
35
+ isCodeView: false,
36
+ hasInit: false,
37
+ undoStack: [],
38
+ redoStack: [],
17
39
  rangeCount: 0,
18
- content: '',
40
+ range: null,
41
+ modal
19
42
  })
20
43
 
21
- const updateListStyles = (styles: Set<string>) => {
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
- const updateSelection = () => {
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
- const format = () => formatting(state)
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
- const updateContent = (type?: string) => {
99
- if (!state.doc) return
100
- if (type === 'html') {
101
- state.doc.body.innerHTML = state.content
102
- } else {
103
- state.content = state.doc.body.innerHTML
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 === '&nbsp;' || 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
- updateActiveStyles()
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
- const handleToolbarAction = (action: string, value?: string) => {
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
- const applyFormatting = (command: string, value?: string) => {
124
- const { selection, range } = state
125
- if (!state.doc || !state.modal || !selection || !range) return
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
- if (!state.doc.body.contains(selection.anchorNode)) {
131
- state.doc.body.focus()
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
- console.log('command', command, value)
134
- switch (command) {
135
- case 'insertTable': {
136
- const [rows, cols] = value?.split('x').map(Number) || [3, 3]
137
- insertTable(rows, cols, state)
138
- break
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
- case 'mergeCells':
141
- if (isCaret) return
142
- mergeCells(range, state.doc)
143
- break
144
- case 'splitCells':
145
- if (isCaret) return
146
- splitCell(range, state.doc)
147
- break
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
- case 'orderedList':
171
- case 'unorderedList':
172
- format().list(command)
173
- break
174
- case 'image':
175
- insertImage(state.modal, state)
176
- break
177
- // case 'youtube':
178
- // if (isCaret) return
179
- // insertImage(state.modal, state.doc, range)
180
- // break
181
- case 'link':
182
- if (isCaret) return
183
- insertLink(state.modal, state)
184
- break
185
-
186
- case 'blockquote':
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
  }