@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.
- package/dist/components/Btn.vue.d.ts.map +1 -1
- package/dist/components/Carousel.vue.d.ts +1 -1
- package/dist/components/Modal.vue.d.ts +3 -0
- package/dist/components/Modal.vue.d.ts.map +1 -1
- package/dist/components/Slider.vue.d.ts +1 -1
- package/dist/components/Slider.vue.d.ts.map +1 -1
- package/dist/components/analytics/BarChart.vue.d.ts +11 -3
- package/dist/components/analytics/BarChart.vue.d.ts.map +1 -1
- package/dist/components/analytics/LineChart.vue.d.ts +9 -0
- package/dist/components/analytics/LineChart.vue.d.ts.map +1 -1
- package/dist/components/analytics/PieChart.vue.d.ts +30 -2
- package/dist/components/analytics/PieChart.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +8 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts +9 -0
- package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +0 -14
- 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 +15 -15
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -3
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/media-clean.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/media-clean.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/media.d.ts +4 -4
- package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
- package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
- package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
- package/dist/index.cjs +123 -22
- package/dist/index.mjs +123 -22
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Btn.vue +50 -42
- package/src/components/Modal.vue +49 -50
- package/src/components/analytics/BarChart.vue +118 -7
- package/src/components/analytics/KpiCard.vue +2 -2
- package/src/components/analytics/LineChart.vue +189 -105
- package/src/components/analytics/PieChart.vue +392 -49
- package/src/components/dataTable/DataTable.vue +1 -1
- package/src/components/form/inputs/RichText/CheckList.md +23 -0
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +243 -27
- package/src/components/form/inputs/RichText/components/TableGridSelector.vue +94 -0
- package/src/components/form/inputs/RichText/composables/useCommands.ts +45 -0
- package/src/components/form/inputs/RichText/composables/useEditor.ts +13 -10
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +3 -128
- package/src/components/form/inputs/RichText/config.ts +33 -10
- package/src/components/form/inputs/RichText/editor.css +300 -33
- package/src/components/form/inputs/RichText/index.vue +3271 -130
- package/src/components/form/inputs/RichText/richTextTypes.ts +7 -3
- package/src/components/form/inputs/RichText/utils/commands.ts +851 -90
- package/src/components/form/inputs/RichText/utils/formatting.ts +17 -15
- package/src/components/form/inputs/RichText/utils/media-clean.ts +0 -0
- package/src/components/form/inputs/RichText/utils/media.ts +133 -67
- package/src/components/form/inputs/RichText/utils/selection.ts +40 -11
- package/src/components/form/inputs/RichText/utils/table.ts +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/AppContent.vue +26 -26
- package/src/components/layout/AppLayout.vue +21 -3
- package/src/components/layout/AppSidebar.vue +5 -2
- package/src/styles/layout.css +267 -0
- package/src/styles/mobilLayout.css +266 -0
- 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
|
|
5
|
+
import TableGridSelector from './TableGridSelector.vue'
|
|
6
6
|
|
|
7
|
-
const {
|
|
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
|
-
|
|
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 = ' '
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
155
|
-
|
|
156
|
-
updateState.styles()
|
|
157
|
-
})
|
|
159
|
+
// Update styles immediately for better responsiveness
|
|
160
|
+
updateState.styles()
|
|
158
161
|
}
|
|
159
|
-
} catch
|
|
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
|
-
//
|
|
35
|
-
|
|
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 )
|
|
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 ), consider it empty
|
|
180
|
-
return brElements.length === 1 && node.childNodes.length <= 2 // Allow for + <br>
|
|
181
|
-
}
|