@bagelink/vue 1.4.139 → 1.4.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/components/Btn.vue.d.ts.map +1 -1
  2. package/dist/components/Carousel.vue.d.ts +1 -1
  3. package/dist/components/Modal.vue.d.ts +3 -0
  4. package/dist/components/Modal.vue.d.ts.map +1 -1
  5. package/dist/components/Slider.vue.d.ts +1 -1
  6. package/dist/components/Slider.vue.d.ts.map +1 -1
  7. package/dist/components/analytics/BarChart.vue.d.ts +11 -3
  8. package/dist/components/analytics/BarChart.vue.d.ts.map +1 -1
  9. package/dist/components/analytics/LineChart.vue.d.ts +9 -0
  10. package/dist/components/analytics/LineChart.vue.d.ts.map +1 -1
  11. package/dist/components/analytics/PieChart.vue.d.ts +30 -2
  12. package/dist/components/analytics/PieChart.vue.d.ts.map +1 -1
  13. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +8 -0
  14. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
  15. package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts +9 -0
  16. package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts.map +1 -0
  17. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
  18. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +0 -14
  19. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  20. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  21. package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
  22. package/dist/components/form/inputs/RichText/index.vue.d.ts +15 -15
  23. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  24. package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -3
  25. package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
  26. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  27. package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -1
  28. package/dist/components/form/inputs/RichText/utils/media-clean.d.ts +2 -0
  29. package/dist/components/form/inputs/RichText/utils/media-clean.d.ts.map +1 -0
  30. package/dist/components/form/inputs/RichText/utils/media.d.ts +4 -4
  31. package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
  32. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  33. package/dist/components/form/inputs/RichText/utils/table.d.ts +1 -1
  34. package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
  35. package/dist/components/index.d.ts +1 -0
  36. package/dist/components/index.d.ts.map +1 -1
  37. package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
  38. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
  39. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  40. package/dist/index.cjs +123 -22
  41. package/dist/index.mjs +123 -22
  42. package/dist/style.css +1 -1
  43. package/package.json +1 -1
  44. package/src/components/Btn.vue +50 -42
  45. package/src/components/Modal.vue +49 -50
  46. package/src/components/analytics/BarChart.vue +118 -7
  47. package/src/components/analytics/KpiCard.vue +2 -2
  48. package/src/components/analytics/LineChart.vue +189 -105
  49. package/src/components/analytics/PieChart.vue +392 -49
  50. package/src/components/dataTable/DataTable.vue +1 -1
  51. package/src/components/form/inputs/RichText/CheckList.md +23 -0
  52. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +243 -27
  53. package/src/components/form/inputs/RichText/components/TableGridSelector.vue +94 -0
  54. package/src/components/form/inputs/RichText/composables/useCommands.ts +45 -0
  55. package/src/components/form/inputs/RichText/composables/useEditor.ts +13 -10
  56. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +3 -128
  57. package/src/components/form/inputs/RichText/config.ts +33 -10
  58. package/src/components/form/inputs/RichText/editor.css +300 -33
  59. package/src/components/form/inputs/RichText/index.vue +3271 -130
  60. package/src/components/form/inputs/RichText/richTextTypes.ts +7 -3
  61. package/src/components/form/inputs/RichText/utils/commands.ts +851 -90
  62. package/src/components/form/inputs/RichText/utils/formatting.ts +17 -15
  63. package/src/components/form/inputs/RichText/utils/media-clean.ts +0 -0
  64. package/src/components/form/inputs/RichText/utils/media.ts +133 -67
  65. package/src/components/form/inputs/RichText/utils/selection.ts +40 -11
  66. package/src/components/form/inputs/RichText/utils/table.ts +1 -1
  67. package/src/components/index.ts +1 -0
  68. package/src/components/layout/AppContent.vue +26 -26
  69. package/src/components/layout/AppLayout.vue +21 -3
  70. package/src/components/layout/AppSidebar.vue +5 -2
  71. package/src/styles/layout.css +267 -0
  72. package/src/styles/mobilLayout.css +266 -0
  73. package/src/styles/modal.css +3 -17
@@ -2,44 +2,240 @@
2
2
  import type { ToolbarConfig, ToolbarConfigOption, ToolbarOption } from '../richTextTypes'
3
3
  import { Btn, Dropdown } from '@bagelink/vue'
4
4
  import { basicToolbarConfig, toolbarOptions } from '../config'
5
- import GridBox from './gridBox.vue'
5
+ import TableGridSelector from './TableGridSelector.vue'
6
6
 
7
- const { config = basicToolbarConfig, selectedStyles } = defineProps<{
7
+ const {
8
+ config = basicToolbarConfig,
9
+ selectedStyles,
10
+ hideImages = false,
11
+ hideVideos = false,
12
+ hideEmbeds = false,
13
+ hideTables = false,
14
+ hideAlignment = false,
15
+ hideDirections = false,
16
+ hideH5H6 = false,
17
+ hide = []
18
+ } = defineProps<{
8
19
  config?: ToolbarConfig
9
20
  selectedStyles: Set<string>
21
+ hideImages?: boolean
22
+ hideVideos?: boolean
23
+ hideEmbeds?: boolean
24
+ hideTables?: boolean
25
+ hideAlignment?: boolean
26
+ hideDirections?: boolean
27
+ hideH5H6?: boolean
28
+ hide?: string[]
10
29
  }>()
11
30
  const emit = defineEmits(['action'])
12
31
 
13
- const configToOption = (action: ToolbarConfigOption) => toolbarOptions.find(option => option.name === action) as ToolbarOption
32
+ // Function to get the current alignment icon based on active styles
33
+ function getCurrentAlignmentIcon(): string {
34
+ if (selectedStyles.has('alignLeft')) return 'format_align_left'
35
+ if (selectedStyles.has('alignCenter')) return 'format_align_center'
36
+ if (selectedStyles.has('alignRight')) return 'format_align_right'
37
+ if (selectedStyles.has('alignJustify')) return 'format_align_justify'
38
+ return 'format_align_left' // default
39
+ }
40
+
41
+ // Function to check if any alignment is active
42
+ function isAlignmentActive(): boolean {
43
+ return selectedStyles.has('alignLeft') ||
44
+ selectedStyles.has('alignCenter') ||
45
+ selectedStyles.has('alignRight') ||
46
+ selectedStyles.has('alignJustify')
47
+ }
48
+
49
+ // Helper function to check if an action should be hidden
50
+ function shouldHideAction(actionName: string): boolean {
51
+ // Check if it's in the hide array - this covers ALL items including:
52
+ // bold, italic, underline, h1, h2, h3, h4, h5, h6, link, image, video, embed,
53
+ // ul, ol, blockquote, code, clear, direction, table, fullScreen
54
+ if (hide.includes(actionName)) return true
55
+
56
+ // Alternative name mappings for some actions
57
+ const alternativeNames: Record<string, string[]> = {
58
+ 'image': ['insertImage', 'image'],
59
+ 'video': ['insertVideo', 'video'],
60
+ 'embed': ['insertEmbed', 'embed'],
61
+ 'table': ['insertTable'],
62
+ 'direction': ['textDirection'],
63
+ 'fullScreen': ['fullScreen'],
64
+ 'ul': ['unorderedList'],
65
+ 'ol': ['orderedList'],
66
+ 'splitView': ['splitView'],
67
+ 'p': ['p'],
68
+ 'align': ['alignMenu'],
69
+ 'alignment': ['alignMenu']
70
+ }
71
+
72
+ // Check alternative names
73
+ for (const [hideKey, actionNames] of Object.entries(alternativeNames)) {
74
+ if (hide.includes(hideKey) && actionNames.includes(actionName)) {
75
+ return true
76
+ }
77
+ }
78
+
79
+ // Map action names to check against specific hide props (for backward compatibility)
80
+ const actionMap: Record<string, boolean> = {
81
+ 'insertImage': hideImages,
82
+ 'insertVideo': hideVideos,
83
+ 'insertEmbed': hideEmbeds,
84
+ 'insertTable': hideTables,
85
+ 'alignMenu': hideAlignment,
86
+ 'textDirection': hideDirections,
87
+ 'h5': hideH5H6,
88
+ 'h6': hideH5H6
89
+ }
90
+
91
+ return actionMap[actionName] || false
92
+ }
93
+
94
+ // Helper function to check if a separator should be shown
95
+ function shouldShowSeparator(currentIndex: number): boolean {
96
+ const allActions = config.map(configToOption).filter(Boolean)
97
+
98
+ // Simple approach: find the last visible item before this separator
99
+ let lastVisibleBeforeIndex = -1
100
+ for (let i = currentIndex - 1; i >= 0; i--) {
101
+ const action = allActions[i]
102
+ if (action && action.name !== 'separator' && !shouldHideAction(action.name)) {
103
+ lastVisibleBeforeIndex = i
104
+ break
105
+ }
106
+ }
107
+
108
+ // Find the first visible item after this separator
109
+ let firstVisibleAfterIndex = -1
110
+ for (let i = currentIndex + 1; i < allActions.length; i++) {
111
+ const action = allActions[i]
112
+ if (action && action.name !== 'separator' && !shouldHideAction(action.name)) {
113
+ firstVisibleAfterIndex = i
114
+ break
115
+ }
116
+ }
117
+
118
+ // Don't show if we don't have visible items on both sides
119
+ if (lastVisibleBeforeIndex === -1 || firstVisibleAfterIndex === -1) {
120
+ return false
121
+ }
122
+
123
+ // Check if there's already a visible separator between these items
124
+ for (let i = lastVisibleBeforeIndex + 1; i < currentIndex; i++) {
125
+ const action = allActions[i]
126
+ if (action && action.name === 'separator') {
127
+ // There's already a separator closer to the visible items
128
+ return false
129
+ }
130
+ }
131
+
132
+ return true
133
+ } const configToOption = (action: ToolbarConfigOption) => toolbarOptions.find(option => option.name === action) as ToolbarOption
14
134
 
15
135
  function runAction(name: ToolbarConfigOption, value?: string) {
136
+ console.log('EditorToolbar: runAction called', { name, value })
16
137
  emit('action', name, value)
17
138
  }
139
+
140
+ function handleQuickTableInsert(rows: number, cols: number) {
141
+ // Create a simple table quickly
142
+ const table = document.createElement('table')
143
+ table.style.width = '100%'
144
+ table.style.borderCollapse = 'collapse'
145
+ table.style.marginBottom = '1rem'
146
+
147
+ // Add header
148
+ const thead = document.createElement('thead')
149
+ const headerRow = thead.insertRow()
150
+ for (let j = 0; j < cols; j++) {
151
+ const th = document.createElement('th')
152
+ th.innerHTML = `Header ${j + 1}`
153
+ th.style.padding = '8px'
154
+ th.style.border = '1px solid #ddd'
155
+ th.style.backgroundColor = '#f4f4f4'
156
+ headerRow.appendChild(th)
157
+ }
158
+ table.appendChild(thead)
159
+
160
+ // Add body
161
+ const tbody = document.createElement('tbody')
162
+ for (let i = 0; i < rows; i++) {
163
+ const row = tbody.insertRow()
164
+ for (let j = 0; j < cols; j++) {
165
+ const cell = row.insertCell()
166
+ cell.innerHTML = '&nbsp;'
167
+ cell.style.padding = '8px'
168
+ cell.style.border = '1px solid #ddd'
169
+ }
170
+ }
171
+ table.appendChild(tbody)
172
+
173
+ emit('action', 'insertTable', table.outerHTML)
174
+ }
175
+
176
+ function handleOpenAdvanced() {
177
+ // The advanced table editor is now handled by the main RichText component
178
+ // through the insertTable command which opens the main modal
179
+ emit('action', 'insertTable')
180
+ }
18
181
  </script>
19
182
 
20
183
  <template>
21
- <div class="toolbar flex gap-025 pb-05 flex-wrap" role="toolbar">
22
- <template v-for="(action, index) in config.map(configToOption).filter(Boolean)" :key="index">
23
- <Dropdown v-if="action.name === 'insertTable'" placement="bottom-start" thin flat icon="table">
24
- <template #default="{ hide }">
25
- <GridBox
26
- :gridSize="5" @select="(value) => {
27
- runAction('insertTable', value);
28
- hide();
29
- }"
30
- />
31
- </template>
32
- </Dropdown>
33
- <Btn
34
- v-else-if="action.name !== 'separator'" v-tooltip="action.label" :icon="action.icon" thin flat
35
- :aria-label="action.name" :class="[action.class, { active: selectedStyles.has(action.name) }]"
36
- class=""
37
- tabindex="-1"
38
- @click="runAction(action.name)"
39
- />
40
- <span v-else-if="action.name === 'separator'" :key="`separator-${index}`" class="opacity-2 mb-025">|</span>
41
- </template>
42
- </div>
184
+ <div class="toolbar flex gap-025 pb-05 flex-wrap" role="toolbar" style="position: relative;">
185
+ <template v-for="(action, index) in config.map(configToOption).filter(Boolean)" :key="index">
186
+ <!-- Tables -->
187
+ <TableGridSelector v-if="action.name === 'insertTable' && !shouldHideAction('insertTable')" @insert="handleQuickTableInsert" @open-advanced="handleOpenAdvanced" />
188
+ <!-- Images -->
189
+ <Btn v-else-if="action.name === 'insertImage' && !shouldHideAction('insertImage')" v-tooltip="action.label" :icon="action.icon" thin flat :aria-label="action.name"
190
+ :class="[action.class, { active: selectedStyles.has(action.name) }]" class="" tabindex="-1" @click="runAction(action.name)" />
191
+
192
+ <!-- Videos -->
193
+ <Btn v-else-if="action.name === 'insertVideo' && !shouldHideAction('insertVideo')" v-tooltip="action.label" :icon="action.icon" thin flat :aria-label="action.name"
194
+ :class="[action.class, { active: selectedStyles.has(action.name) }]" class="" tabindex="-1" @click="runAction(action.name)" />
195
+
196
+ <!-- Embeds -->
197
+ <Btn v-else-if="action.name === 'insertEmbed' && !shouldHideAction('insertEmbed')" v-tooltip="action.label" :icon="action.icon" thin flat :aria-label="action.name"
198
+ :class="[action.class, { active: selectedStyles.has(action.name) }]" class="" tabindex="-1" @click="runAction(action.name)" />
199
+
200
+ <!-- Alignment Menu -->
201
+ <Dropdown v-else-if="action.name === 'alignMenu' && !shouldHideAction('alignMenu')" placement="bottom-start" thin flat :icon="getCurrentAlignmentIcon()"
202
+ :class="{ 'alignment-active': isAlignmentActive() }">
203
+ <template #default="{ hide }">
204
+ <div class="flex flex-column p-025">
205
+ <Btn thin flat icon="format_align_left" :class="{ active: selectedStyles.has('alignLeft') }" @click="runAction('alignLeft'); hide()" />
206
+ <Btn thin flat icon="format_align_center" :class="{ active: selectedStyles.has('alignCenter') }" @click="runAction('alignCenter'); hide()" />
207
+ <Btn thin flat icon="format_align_right" :class="{ active: selectedStyles.has('alignRight') }" @click="runAction('alignRight'); hide()" />
208
+ <Btn thin flat icon="format_align_justify" :class="{ active: selectedStyles.has('alignJustify') }" @click="runAction('alignJustify'); hide()" />
209
+ </div>
210
+ </template>
211
+ </Dropdown>
212
+
213
+ <!-- Text Direction -->
214
+ <Btn v-else-if="action.name === 'textDirection' && !shouldHideAction('textDirection')" v-tooltip="action.label"
215
+ :icon="selectedStyles.has('textDirection') ? 'format_textdirection_l_to_r' : 'format_textdirection_r_to_l'" thin flat :aria-label="action.name"
216
+ :class="[action.class, { active: selectedStyles.has('textDirection') }]" class="" tabindex="-1" @click="runAction(action.name)" />
217
+
218
+ <!-- H5 and H6 -->
219
+ <Btn v-else-if="(action.name === 'h5' || action.name === 'h6') && !shouldHideAction(action.name)" v-tooltip="action.label" :icon="action.icon" thin flat :aria-label="action.name"
220
+ :class="[action.class, { active: selectedStyles.has(action.name) }]" class="" tabindex="-1" @click="runAction(action.name)" />
221
+
222
+ <!-- All other buttons - check if they should be hidden by the hide array -->
223
+ <Btn v-else-if="action.name !== 'separator' &&
224
+ action.name !== 'insertTable' &&
225
+ action.name !== 'insertImage' &&
226
+ action.name !== 'insertVideo' &&
227
+ action.name !== 'insertEmbed' &&
228
+ action.name !== 'alignMenu' &&
229
+ action.name !== 'textDirection' &&
230
+ action.name !== 'h5' &&
231
+ action.name !== 'h6' &&
232
+ !shouldHideAction(action.name)" v-tooltip="action.label" :icon="action.icon" thin flat :aria-label="action.name" :class="[action.class, { active: selectedStyles.has(action.name) }]" class=""
233
+ tabindex="-1" @click="runAction(action.name)" />
234
+
235
+ <!-- Separator -->
236
+ <span v-else-if="action.name === 'separator' && shouldShowSeparator(index)" :key="`separator-${index}`" class="opacity-2 mb-025">|</span>
237
+ </template>
238
+ </div>
43
239
  </template>
44
240
 
45
241
  <style scoped>
@@ -48,8 +244,28 @@ function runAction(name: ToolbarConfigOption, value?: string) {
48
244
  color: white;
49
245
  }
50
246
  .toolbar :deep(.active):hover {
51
- color: var(--bgl-black);
247
+ background: var(--bgl-primary) !important;
248
+ color: white;
249
+ }
250
+ /* Headings menu styling */
251
+ .toolbar :deep(.dropdown-content) {
252
+ min-width: 40px;
253
+ }
254
+
255
+ .toolbar :deep(.dropdown-content .btn) {
256
+ min-width: 32px;
257
+ height: 32px;
258
+ padding: 0;
259
+ }
260
+ /* Alignment active state */
261
+ .alignment-active :deep(button) {
262
+ background: var(--bgl-primary) !important;
263
+ color: white !important;
264
+ }
265
+
266
+ .alignment-active :deep(button:hover) {
267
+ background: var(--bgl-primary) !important;
268
+ color: white !important;
52
269
  }
53
270
  </style>
54
271
 
55
- <style scoped></style>
@@ -0,0 +1,94 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import { Btn, Dropdown } from '@bagelink/vue'
4
+
5
+
6
+ const emit = defineEmits<{
7
+ insert: [rows: number, cols: number]
8
+ openAdvanced: []
9
+ }>()
10
+
11
+ const hoveredRow = ref(0)
12
+ const hoveredCol = ref(0)
13
+
14
+ const maxRows = 10
15
+ const maxCols = 10
16
+
17
+ const gridCells = computed(() => {
18
+ const cells = []
19
+ for (let row = 1; row <= maxRows; row++) {
20
+ for (let col = 1; col <= maxCols; col++) {
21
+ cells.push({
22
+ row,
23
+ col,
24
+ isActive: row <= hoveredRow.value && col <= hoveredCol.value
25
+ })
26
+ }
27
+ }
28
+ return cells
29
+ })
30
+
31
+ const selectionText = computed(() => {
32
+ if (hoveredRow.value === 0 || hoveredCol.value === 0) {
33
+ return 'Select table size'
34
+ }
35
+ return `${hoveredRow.value} × ${hoveredCol.value}`
36
+ })
37
+
38
+ const handleCellHover = (row: number, col: number) => {
39
+ hoveredRow.value = row
40
+ hoveredCol.value = col
41
+ }
42
+
43
+ const handleCellClick = (row: number, col: number, hide: () => void) => {
44
+ emit('insert', row, col)
45
+ hide()
46
+ }
47
+
48
+ const handleAdvanced = (hide: () => void) => {
49
+ emit('openAdvanced')
50
+ hide()
51
+ }
52
+
53
+ const handleMouseLeave = () => {
54
+ hoveredRow.value = 0
55
+ hoveredCol.value = 0
56
+ }
57
+ </script>
58
+
59
+ <template>
60
+ <Dropdown placement="bottom-start" thin flat icon="table">
61
+ <template #default="{ hide }">
62
+ <div class="table-grid-selector p-075">
63
+ <!-- Header -->
64
+ <div class="txt-center mb-075">
65
+ <div style="font-size: 14px; font-weight: 500; color: #333; margin-bottom: 4px;">
66
+ {{ selectionText }}
67
+ </div>
68
+ <div style="font-size: 12px; color: #666;">
69
+ Hover to select size
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Grid -->
74
+ <div style="display: grid; grid-template-columns: repeat(10, 20px); grid-template-rows: repeat(10, 20px); gap: 2px; margin-bottom: 12px; direction: ltr;" @mouseleave="handleMouseLeave">
75
+ <div v-for="cell in gridCells" :key="`${cell.row}-${cell.col}`" @mouseenter="handleCellHover(cell.row, cell.col)" @click="handleCellClick(cell.row, cell.col, hide)" :style="{
76
+ width: '20px',
77
+ height: '20px',
78
+ border: '1px solid #ddd',
79
+ backgroundColor: cell.isActive ? 'var(--bgl-primary)' : '#fff',
80
+ cursor: 'pointer',
81
+ borderRadius: '2px',
82
+ transition: 'background-color 0.1s ease'
83
+ }">
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Advanced button -->
88
+ <div class="border-top pt-075">
89
+ <Btn @click="handleAdvanced(hide)" value="Advanced Settings" icon="settings" class="bg-gray-30 color-black" />
90
+ </div>
91
+ </div>
92
+ </template>
93
+ </Dropdown>
94
+ </template>
@@ -1,10 +1,49 @@
1
1
  import type { EditorState } from '../richTextTypes'
2
2
  import { createCommandExecutor, createCommandRegistry } from '../utils/commands'
3
+ import { isStyleActive } from '../utils/selection'
3
4
 
4
5
  export function useCommands(state: EditorState, debug?: { logCommand: (command: string, value?: string) => void }) {
5
6
  const commands = createCommandRegistry(state)
6
7
  const executor = createCommandExecutor(state, commands)
7
8
 
9
+ // Function to immediately update styles
10
+ const updateStylesImmediately = () => {
11
+ if (!state.doc) return
12
+
13
+ const styles = new Set<string>()
14
+ const styleTypes = [
15
+ 'bold',
16
+ 'italic',
17
+ 'underline',
18
+ 'link',
19
+ 'h1',
20
+ 'h2',
21
+ 'h3',
22
+ 'h4',
23
+ 'h5',
24
+ 'h6',
25
+ 'blockquote',
26
+ 'p',
27
+ 'orderedList',
28
+ 'unorderedList',
29
+ 'alignLeft',
30
+ 'alignCenter',
31
+ 'alignRight',
32
+ 'alignJustify',
33
+ 'textDirection',
34
+ 'ltrDirection',
35
+ 'rtlDirection'
36
+ ]
37
+
38
+ styleTypes.forEach((style) => {
39
+ if (isStyleActive(style, state.doc!)) {
40
+ styles.add(style)
41
+ }
42
+ })
43
+
44
+ state.selectedStyles = styles
45
+ }
46
+
8
47
  return {
9
48
  execute: (command: string, value?: string) => {
10
49
  if (!state.doc) {
@@ -48,6 +87,12 @@ export function useCommands(state: EditorState, debug?: { logCommand: (command:
48
87
  // Execute the command
49
88
  try {
50
89
  executor.execute(command, value)
90
+
91
+ // Update styles immediately after command execution for all commands except view state
92
+ const viewCommands = ['splitView', 'codeView', 'fullScreen']
93
+ if (!viewCommands.includes(command)) {
94
+ updateStylesImmediately()
95
+ }
51
96
  } catch (e) {
52
97
  console.error('[useCommands] Error during command execution:', e)
53
98
  }
@@ -1,5 +1,4 @@
1
1
  import type { EditorState, EditorDebugInterface, EditorDebuggerInstance } from '../richTextTypes'
2
- import { useModal } from '@bagelink/vue'
3
2
  import { reactive } from 'vue'
4
3
  import { EditorDebugger } from '../utils/debug'
5
4
  import { isStyleActive } from '../utils/selection'
@@ -34,7 +33,6 @@ function restoreIframes(doc: Document, content: string, iframes: HTMLIFrameEleme
34
33
  }
35
34
 
36
35
  export function useEditor() {
37
- const modal = useModal()
38
36
  let cleanupListeners: (() => void) | null = null
39
37
 
40
38
  const state = reactive<EditorState>({
@@ -50,7 +48,6 @@ export function useEditor() {
50
48
  redoStack: [],
51
49
  rangeCount: 0,
52
50
  range: null,
53
- modal,
54
51
  debug: undefined
55
52
  })
56
53
 
@@ -72,7 +69,14 @@ export function useEditor() {
72
69
  'blockquote',
73
70
  'p',
74
71
  'orderedList',
75
- 'unorderedList'
72
+ 'unorderedList',
73
+ 'alignLeft',
74
+ 'alignCenter',
75
+ 'alignRight',
76
+ 'alignJustify',
77
+ 'textDirection',
78
+ 'ltrDirection',
79
+ 'rtlDirection'
76
80
  ]
77
81
  styleTypes.forEach((style) => {
78
82
  if (state.doc && isStyleActive(style, state.doc)) {
@@ -110,7 +114,8 @@ export function useEditor() {
110
114
  try {
111
115
  selection.removeAllRanges()
112
116
  selection.addRange(range)
113
- } catch (e) {
117
+ } catch {
118
+ // Range restoration failed, ignore
114
119
  }
115
120
  }
116
121
  }
@@ -151,12 +156,10 @@ export function useEditor() {
151
156
  state.range = newSelection.getRangeAt(0).cloneRange()
152
157
  }
153
158
 
154
- // Update styles less frequently
155
- requestAnimationFrame(() => {
156
- updateState.styles()
157
- })
159
+ // Update styles immediately for better responsiveness
160
+ updateState.styles()
158
161
  }
159
- } catch (e) {
162
+ } catch {
160
163
  state.selection = null
161
164
  state.range = null
162
165
  state.rangeCount = 0
@@ -14,6 +14,7 @@ const shortcuts: KeyboardShortcut[] = [
14
14
  { key: 'b', command: 'bold' },
15
15
  { key: 'i', command: 'italic' },
16
16
  { key: 'u', command: 'underline' },
17
+ { key: 'k', command: 'link' },
17
18
  { key: 'z', command: 'undo' },
18
19
  { key: 'z', modifiers: { shift: true }, command: 'redo' },
19
20
  { key: 'y', command: 'redo' },
@@ -31,105 +32,8 @@ const shortcuts: KeyboardShortcut[] = [
31
32
  export function useEditorKeyboard(doc: Document, executor: CommandExecutor): void {
32
33
  // Handle keyboard shortcuts
33
34
  doc.addEventListener('keydown', (e) => {
34
- // Handle Enter key in lists and empty editor
35
- if (e.key === 'Enter' && !e.shiftKey) {
36
- const selection = doc.getSelection()
37
- if (!selection || !selection.rangeCount) return
38
-
39
- const range = selection.getRangeAt(0)
40
- const container = range.commonAncestorContainer
41
- const listItem = (container.nodeType === 3 ? container.parentElement : container as Element)?.closest('li')
42
-
43
- if (listItem) {
44
- // If we're at the end of a list item
45
- if (range.collapsed && isAtEndOfNode(listItem, range)) {
46
- // If the list item is empty, break out of the list
47
- if (isNodeEmpty(listItem)) {
48
- e.preventDefault()
49
- // Create a new paragraph after the list
50
- const list = listItem.parentElement
51
- if (!list) return
52
-
53
- // Remove the empty list item
54
- listItem.remove()
55
-
56
- // If the list is now empty, remove it
57
- if (!list.querySelector('li')) {
58
- const p = doc.createElement('p')
59
- p.innerHTML = ''
60
- list.parentNode?.replaceChild(p, list)
61
-
62
- // Set cursor in the new paragraph
63
- range.selectNodeContents(p)
64
- range.collapse(true)
65
- selection.removeAllRanges()
66
- selection.addRange(range)
67
- }
68
- } else {
69
- // Create a new list item
70
- e.preventDefault()
71
- const newLi = doc.createElement('li')
72
- newLi.innerHTML = '' // Start with empty list item
73
- listItem.insertAdjacentElement('afterend', newLi)
74
-
75
- // Move cursor to new list item
76
- range.selectNodeContents(newLi)
77
- range.collapse(true)
78
- selection.removeAllRanges()
79
- selection.addRange(range)
80
- }
81
- }
82
- } else {
83
- // Handle Enter in regular content - create new paragraph
84
- const blockElement = (container.nodeType === 3 ? container.parentElement : container as Element)?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,div')
85
-
86
- if (blockElement && range.collapsed) {
87
- // If we're at the end of a block element, create a new paragraph
88
- if (isAtEndOfNode(blockElement, range)) {
89
- e.preventDefault()
90
- const newP = doc.createElement('p')
91
- newP.innerHTML = ''
92
- blockElement.insertAdjacentElement('afterend', newP)
93
-
94
- // Move cursor to new paragraph
95
- range.selectNodeContents(newP)
96
- range.collapse(true)
97
- selection.removeAllRanges()
98
- selection.addRange(range)
99
- }
100
- } else if (!blockElement && doc.body.textContent?.trim()) {
101
- // If we're typing directly in the body and press Enter, wrap in paragraphs
102
- e.preventDefault()
103
-
104
- // Split content at cursor position
105
- const textContent = doc.body.textContent || ''
106
- const cursorPos = range.startOffset
107
- const beforeText = textContent.substring(0, cursorPos).trim()
108
- const afterText = textContent.substring(cursorPos).trim()
109
-
110
- // Clear body
111
- doc.body.innerHTML = ''
112
-
113
- // Create first paragraph with before text
114
- if (beforeText) {
115
- const p1 = doc.createElement('p')
116
- p1.textContent = beforeText
117
- doc.body.appendChild(p1)
118
- }
119
-
120
- // Create second paragraph for after text
121
- const p2 = doc.createElement('p')
122
- p2.textContent = afterText
123
- doc.body.appendChild(p2)
124
-
125
- // Set cursor at beginning of second paragraph
126
- range.selectNodeContents(p2)
127
- range.collapse(true)
128
- selection.removeAllRanges()
129
- selection.addRange(range)
130
- }
131
- }
132
- }
35
+ // Remove Enter key handling from here - it's handled in setupAutoWrapping in index.vue
36
+ // This was causing conflicts with the main Enter key handler
133
37
 
134
38
  // Handle other keyboard shortcuts
135
39
  if (!e.ctrlKey && !e.metaKey) return
@@ -150,32 +54,3 @@ export function useEditorKeyboard(doc: Document, executor: CommandExecutor): voi
150
54
  }
151
55
  })
152
56
  }
153
-
154
- // Helper function to check if we're at the end of a node
155
- function isAtEndOfNode(node: Node, range: Range): boolean {
156
- if (node.nodeType === 3) { // Text node
157
- return range.startOffset === (node as Text).length
158
- }
159
-
160
- const { lastChild } = node
161
- if (!lastChild) return true
162
-
163
- if (lastChild.nodeType === 3) { // Text node
164
- return range.startContainer === lastChild && range.startOffset === lastChild.textContent?.length
165
- }
166
-
167
- return range.startContainer === node && range.startOffset === node.childNodes.length
168
- }
169
-
170
- // Helper function to check if a node is empty (contains only whitespace or <br> or &nbsp;)
171
- function isNodeEmpty(node: Node): boolean {
172
- const text = node.textContent?.replace(/\s/g, '') || '' // Remove non-breaking spaces and whitespace
173
- if (text) return false
174
-
175
- // Check for <br> tags
176
- const brElements = (node as Element).getElementsByTagName('br')
177
- if (brElements.length === 0) return true
178
-
179
- // If there's only one <br> and it's the only content (besides potential &nbsp;), consider it empty
180
- return brElements.length === 1 && node.childNodes.length <= 2 // Allow for &nbsp; + <br>
181
- }