@bagelink/vue 0.0.1260 → 0.0.1268

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 (56) hide show
  1. package/dist/components/AddressSearch.vue.d.ts +6 -0
  2. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  3. package/dist/components/DataTable/DataTable.vue.d.ts.map +1 -1
  4. package/dist/components/DropDown.vue.d.ts +51 -48
  5. package/dist/components/DropDown.vue.d.ts.map +1 -1
  6. package/dist/components/form/BagelForm.vue.d.ts.map +1 -1
  7. package/dist/components/form/FieldArray.vue.d.ts.map +1 -1
  8. package/dist/components/form/inputs/DateInput.vue.d.ts +4 -1
  9. package/dist/components/form/inputs/DateInput.vue.d.ts.map +1 -1
  10. package/dist/components/form/inputs/PasswordInput.vue.d.ts.map +1 -1
  11. package/dist/components/form/inputs/RadioGroup.vue.d.ts +1 -1
  12. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
  13. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +31 -23
  14. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  15. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +2 -1
  16. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  17. package/dist/components/form/inputs/RichText/config.d.ts +2 -1
  18. package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
  19. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  20. package/dist/components/form/inputs/RichText/utils/commands.d.ts +1 -0
  21. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  22. package/dist/components/form/inputs/RichText/utils/media.d.ts +5 -3
  23. package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
  24. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  25. package/dist/components/form/inputs/SelectInput.vue.d.ts +12 -0
  26. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  27. package/dist/components/form/inputs/TelInput.vue.d.ts +9 -3
  28. package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -1
  29. package/dist/components/form/useBagelFormState.d.ts.map +1 -1
  30. package/dist/editor-7QC0nG_c.js +4 -0
  31. package/dist/editor-CpMNx6Eo.cjs +4 -0
  32. package/dist/index.cjs +1731 -1191
  33. package/dist/index.mjs +1732 -1192
  34. package/dist/style.css +90 -83
  35. package/dist/utils/index.d.ts +1 -0
  36. package/dist/utils/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/components/DataTable/DataTable.vue +7 -1
  39. package/src/components/Dropdown.vue +5 -2
  40. package/src/components/form/BagelForm.vue +2 -13
  41. package/src/components/form/FieldArray.vue +3 -0
  42. package/src/components/form/inputs/DateInput.vue +341 -162
  43. package/src/components/form/inputs/PasswordInput.vue +5 -1
  44. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +2 -2
  45. package/src/components/form/inputs/RichText/composables/useCommands.ts +53 -97
  46. package/src/components/form/inputs/RichText/composables/useEditor.ts +377 -270
  47. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +124 -58
  48. package/src/components/form/inputs/RichText/config.ts +27 -3
  49. package/src/components/form/inputs/RichText/editor.css +29 -0
  50. package/src/components/form/inputs/RichText/index.vue +129 -55
  51. package/src/components/form/inputs/RichText/richTextTypes.d.ts +35 -49
  52. package/src/components/form/inputs/RichText/utils/commands.ts +181 -0
  53. package/src/components/form/inputs/RichText/utils/media.ts +64 -3
  54. package/src/components/form/inputs/RichText/utils/selection.ts +40 -5
  55. package/src/components/form/useBagelFormState.ts +2 -14
  56. package/src/utils/index.ts +15 -0
@@ -1,4 +1,7 @@
1
1
  import type { EditorState } from '../richTextTypes'
2
+ import { formatting } from './formatting'
3
+ import { insertImage, insertLink, insertEmbed } from './media'
4
+ import { addRow, deleteRow, mergeCells, splitCell, insertTable, deleteTable, insertColumn, deleteColumn, alignColumn } from './table'
2
5
 
3
6
  export interface Command {
4
7
  name: string
@@ -17,6 +20,184 @@ export interface CommandExecutor {
17
20
  getValue: (command: string) => string | null
18
21
  }
19
22
 
23
+ // Centralized command creation helper
24
+ function createCommand(name: string, execute: Command['execute'], isActive?: Command['isActive']): Command {
25
+ return {
26
+ name,
27
+ execute: (state: EditorState, value?: string) => {
28
+ if (!state.doc) return
29
+ execute(state, value)
30
+ },
31
+ isActive
32
+ }
33
+ }
34
+
35
+ // Create formatting commands helper
36
+ function createFormattingCommand(state: EditorState, type: 'text' | 'block' | 'list', command: string, tag?: string): Command {
37
+ const format = formatting(state)
38
+ return createCommand(
39
+ command,
40
+ () => {
41
+ if (!state.doc) return
42
+
43
+ if (type === 'text') {
44
+ if (command === 'bold') state.doc.execCommand('bold', false)
45
+ else if (command === 'italic') state.doc.execCommand('italic', false)
46
+ else if (command === 'underline') state.doc.execCommand('underline', false)
47
+ else format.text(command)
48
+ }
49
+ else if (type === 'block') {
50
+ state.doc.execCommand('formatBlock', false, `<${tag || command}>`)
51
+ }
52
+ else if (type === 'list') {
53
+ const selection = state.doc.getSelection()
54
+ if (!selection || !selection.rangeCount) return
55
+
56
+ const range = selection.getRangeAt(0)
57
+
58
+ // If there's no content or the selection is collapsed
59
+ if (range.collapsed && (!range.startContainer.textContent?.trim() || range.startContainer === state.doc.body)) {
60
+ // Create a new list with an empty item
61
+ const list = state.doc.createElement(command === 'orderedList' ? 'ol' : 'ul')
62
+ const li = state.doc.createElement('li')
63
+ // Use a non-breaking space with br to ensure proper rendering
64
+ li.innerHTML = '&nbsp;<br>'
65
+ list.appendChild(li)
66
+
67
+ // If we're in an empty paragraph, replace it
68
+ const currentBlock = range.startContainer.nodeType === 1
69
+ ? range.startContainer as Element
70
+ : range.startContainer.parentElement
71
+
72
+ if (currentBlock?.tagName.toLowerCase() === 'p' && isNodeEmpty(currentBlock)) {
73
+ currentBlock.parentNode?.replaceChild(list, currentBlock)
74
+ } else {
75
+ // Otherwise insert at cursor
76
+ range.insertNode(list)
77
+ }
78
+
79
+ // Move cursor into the list item
80
+ range.selectNodeContents(li)
81
+ range.collapse(true)
82
+ selection.removeAllRanges()
83
+ selection.addRange(range)
84
+ } else {
85
+ // Use standard command for existing content
86
+ state.doc.execCommand(
87
+ command === 'orderedList' ? 'insertOrderedList' : 'insertUnorderedList',
88
+ false
89
+ )
90
+ }
91
+ }
92
+ },
93
+ () => state.selectedStyles.has(command)
94
+ )
95
+ }
96
+
97
+ // Helper function to check if a node is empty (contains only whitespace or <br>)
98
+ function isNodeEmpty(node: Node): boolean {
99
+ const text = node.textContent?.trim() || ''
100
+ if (text) return false
101
+
102
+ // Check for <br> tags
103
+ const brElements = (node as Element).getElementsByTagName('br')
104
+ if (brElements.length === 0) return true
105
+
106
+ // If there's only one <br> and it's the only content, consider it empty
107
+ return brElements.length === 1 && node.childNodes.length === 1
108
+ }
109
+
110
+ export function createCommandRegistry(state: EditorState): CommandRegistry {
111
+ const format = formatting(state)
112
+
113
+ // History commands
114
+ const historyCommands = {
115
+ undo: createCommand('Undo', () => state.doc?.execCommand('undo', false)),
116
+ redo: createCommand('Redo', () => state.doc?.execCommand('redo', false))
117
+ }
118
+
119
+ // Basic text formatting commands
120
+ const textCommands = ['bold', 'italic', 'underline'].reduce((acc, cmd) => ({
121
+ ...acc,
122
+ [cmd]: createFormattingCommand(state, 'text', cmd)
123
+ }), {})
124
+
125
+ // Heading commands
126
+ const headingCommands = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].reduce((acc, cmd) => ({
127
+ ...acc,
128
+ [cmd]: createFormattingCommand(state, 'block', cmd)
129
+ }), {})
130
+
131
+ // Block commands
132
+ const blockCommands = {
133
+ p: createFormattingCommand(state, 'block', 'p'),
134
+ blockquote: createFormattingCommand(state, 'block', 'blockquote')
135
+ }
136
+
137
+ // List commands
138
+ const listCommands = {
139
+ orderedList: createFormattingCommand(state, 'list', 'orderedList'),
140
+ unorderedList: createFormattingCommand(state, 'list', 'unorderedList')
141
+ }
142
+
143
+ // Table commands
144
+ const tableCommands = {
145
+ insertTable: createCommand('Insert Table', (state, value) => {
146
+ const [rows, cols] = value?.split('x').map(Number) || [3, 3]
147
+ insertTable(rows, cols, state)
148
+ }),
149
+ deleteTable: createCommand('Delete Table', state => state.range && deleteTable(state.range)),
150
+ mergeCells: createCommand('Merge Cells', state => state.range && state.doc && mergeCells(state.range, state.doc)),
151
+ splitCells: createCommand('Split Cells', state => state.range && state.doc && splitCell(state.range, state.doc)),
152
+ addRowBefore: createCommand('Add Row Before', state => state.range && state.doc && addRow('before', state.range, state.doc)),
153
+ addRowAfter: createCommand('Add Row After', state => state.range && state.doc && addRow('after', state.range, state.doc)),
154
+ deleteRow: createCommand('Delete Row', state => state.range && deleteRow(state.range)),
155
+ insertColumnLeft: createCommand('Insert Column Left', state => state.range && insertColumn('before', state.range)),
156
+ insertColumnRight: createCommand('Insert Column Right', state => state.range && insertColumn('after', state.range)),
157
+ deleteColumn: createCommand('Delete Column', state => state.range && deleteColumn(state.range))
158
+ }
159
+
160
+ // Alignment commands
161
+ const alignmentCommands = ['Left', 'Center', 'Right', 'Justify'].reduce((acc, align) => ({
162
+ ...acc,
163
+ [`align${align}`]: createCommand(`Align ${align}`, state => state.range && alignColumn(state.range, align.toLowerCase() as 'left' | 'center' | 'right' | 'justify'))
164
+ }), {})
165
+
166
+ // View state commands
167
+ const viewCommands = {
168
+ fullScreen: createCommand('Full Screen', (state) => { state.isFullscreen = !state.isFullscreen }, state => state.isFullscreen),
169
+ splitView: createCommand('Split View', (state) => { state.isSplitView = !state.isSplitView }, state => state.isSplitView),
170
+ codeView: createCommand('Code View', (state) => { state.isCodeView = !state.isCodeView }, state => state.isCodeView)
171
+ }
172
+
173
+ // Media commands
174
+ const mediaCommands = {
175
+ image: createCommand('Insert Image', state => state.modal && insertImage(state.modal, state)),
176
+ link: createCommand('Insert Link', state => state.modal && state.range && insertLink(state.modal, state)),
177
+ embed: createCommand('Insert Embed', state => state.modal && insertEmbed(state.modal, state))
178
+ }
179
+
180
+ // Other formatting commands
181
+ const otherCommands = {
182
+ clear: createCommand('Clear Formatting', () => { format.clear() }),
183
+ indent: createCommand('Indent', () => { format.text('indent') }),
184
+ outdent: createCommand('Outdent', () => { format.text('outdent') })
185
+ }
186
+
187
+ return {
188
+ ...historyCommands,
189
+ ...textCommands,
190
+ ...headingCommands,
191
+ ...blockCommands,
192
+ ...listCommands,
193
+ ...tableCommands,
194
+ ...alignmentCommands,
195
+ ...viewCommands,
196
+ ...mediaCommands,
197
+ ...otherCommands
198
+ }
199
+ }
200
+
20
201
  export function createCommandExecutor(state: EditorState, commands: CommandRegistry): CommandExecutor {
21
202
  return {
22
203
  execute(command: string, value?: string) {
@@ -1,9 +1,10 @@
1
- import type { EditorState, Modal } from '../richTextTypes'
1
+ import type { Modal } from '@bagelink/vue'
2
+ import type { EditorState } from '../richTextTypes'
2
3
  import { bagelFormUtils } from '@bagelink/vue'
3
4
 
4
5
  const { frmRow, numField } = bagelFormUtils
5
6
 
6
- export function insertImage(modal: Modal, state: EditorState) {
7
+ export function insertImage(modal: typeof Modal, state: EditorState) {
7
8
  const { range, doc } = state
8
9
  if (!range || !doc) return
9
10
 
@@ -45,7 +46,7 @@ export function insertImage(modal: Modal, state: EditorState) {
45
46
  })
46
47
  }
47
48
 
48
- export function insertLink(modal: Modal, state: EditorState) {
49
+ export function insertLink(modal: typeof Modal, state: EditorState) {
49
50
  const { range, doc } = state
50
51
  if (!range || !doc) return
51
52
 
@@ -65,3 +66,63 @@ export function insertLink(modal: Modal, state: EditorState) {
65
66
  }
66
67
  })
67
68
  }
69
+
70
+ export function insertEmbed(modal: typeof Modal, state: EditorState) {
71
+ const { range, doc } = state
72
+ if (!range || !doc) return
73
+
74
+ modal.showModalForm({
75
+ title: 'Insert Embed',
76
+ schema: [
77
+ { id: 'url', $el: 'text', label: 'URL', attrs: { placeholder: 'Enter URL (YouTube, Vimeo, etc.)' } },
78
+ frmRow(
79
+ numField('width', 'Width', { min: 200, placeholder: '560' }),
80
+ numField('height', 'Height', { min: 200, placeholder: '315' })
81
+ ),
82
+ { id: 'allowFullscreen', $el: 'check', label: 'Allow Fullscreen', value: true },
83
+ ],
84
+ onSubmit: (data: { url: string, width?: number, height?: number, allowFullscreen?: boolean }) => {
85
+ if (!data.url) return
86
+
87
+ // Convert common video URLs to embed URLs
88
+ const url = new URL(data.url)
89
+ let embedUrl = data.url
90
+
91
+ // YouTube
92
+ if (url.hostname.includes('youtube.com') || url.hostname === 'youtu.be') {
93
+ const videoId = url.hostname === 'youtu.be'
94
+ ? url.pathname.slice(1)
95
+ : url.searchParams.get('v')
96
+ if (videoId) {
97
+ embedUrl = `https://www.youtube.com/embed/${videoId}`
98
+ }
99
+ }
100
+ // Vimeo
101
+ else if (url.hostname.includes('vimeo.com')) {
102
+ const videoId = url.pathname.split('/').pop()
103
+ if (videoId) {
104
+ embedUrl = `https://player.vimeo.com/video/${videoId}`
105
+ }
106
+ }
107
+
108
+ const iframe = doc.createElement('iframe')
109
+ Object.assign(iframe, {
110
+ src: embedUrl,
111
+ width: data.width || 560,
112
+ height: data.height || 315,
113
+ frameBorder: '0',
114
+ allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
115
+ allowFullscreen: data.allowFullscreen
116
+ })
117
+
118
+ // Create a wrapper div for proper alignment and spacing
119
+ const wrapper = doc.createElement('div')
120
+ wrapper.style.textAlign = 'center'
121
+ wrapper.style.margin = '1em 0'
122
+ wrapper.appendChild(iframe)
123
+
124
+ range.deleteContents()
125
+ range.insertNode(wrapper)
126
+ }
127
+ })
128
+ }
@@ -12,23 +12,58 @@ export function isStyleActive(style: string, doc: Document) {
12
12
 
13
13
  const range = selection.getRangeAt(0)
14
14
  const container = range.commonAncestorContainer
15
- const parent = container.nodeType === 3 ? container.parentElement : container as Element
15
+ const parent = container.nodeType === Node.TEXT_NODE ? container.parentElement : container as Element
16
16
 
17
17
  if (!parent) return false
18
18
 
19
19
  const checkParent = (element: Element | null, tags: string[]): boolean => {
20
- if (!element) return false
21
- if (tags.includes(element.tagName.toLowerCase())) return true
20
+ if (!element || !element.tagName) return false
21
+ const tagName = element.tagName.toLowerCase()
22
+ if (tags.includes(tagName)) return true
22
23
  return checkParent(element.parentElement, tags)
23
24
  }
24
25
 
25
- const styleTags: { [key: string]: string[] } = {
26
+ // Define style mappings for different types of formatting
27
+ const styleMappings: { [key: string]: string[] } = {
28
+ // Inline styles
26
29
  bold: ['strong', 'b'],
27
30
  italic: ['em', 'i'],
28
31
  underline: ['u'],
32
+
33
+ // Block styles
34
+ h1: ['h1'],
35
+ h2: ['h2'],
36
+ h3: ['h3'],
37
+ h4: ['h4'],
38
+ h5: ['h5'],
39
+ h6: ['h6'],
40
+ p: ['p'],
41
+ blockquote: ['blockquote'],
42
+
43
+ // List styles
44
+ orderedList: ['ol'],
45
+ unorderedList: ['ul']
46
+ }
47
+
48
+ // Special handling for view state commands
49
+ if (['splitView', 'codeView', 'fullScreen'].includes(style)) {
50
+ return false // These are handled by the editor state directly
51
+ }
52
+
53
+ // Special handling for list items
54
+ if (style === 'orderedList' || style === 'unorderedList') {
55
+ const listParent = parent.closest(style === 'orderedList' ? 'ol' : 'ul')
56
+ return !!listParent
57
+ }
58
+
59
+ // Check for the style in the style mappings
60
+ const tags = styleMappings[style]
61
+ if (tags) {
62
+ return checkParent(parent, tags)
29
63
  }
30
64
 
31
- return checkParent(parent, styleTags[style] ?? [style])
65
+ // Default to checking the style itself if no mapping exists
66
+ return checkParent(parent, [style])
32
67
  }
33
68
 
34
69
  export interface SelectionInfo {
@@ -1,5 +1,6 @@
1
1
  import type { Ref } from 'vue'
2
2
  import { inject, provide, ref } from 'vue'
3
+ import { getNestedValue } from '../../utils'
3
4
 
4
5
  export const FORM_STATE_KEY = Symbol('bagelFormState')
5
6
 
@@ -30,20 +31,7 @@ export function provideBagelFormState<T>(initialData: T) {
30
31
  const data = ref(initialData) as Ref<T>
31
32
  const isDirty = ref(false)
32
33
 
33
- const getFieldData = (path?: string) => {
34
- if (!path) return ''
35
- const keys = path.split(/[.[]/)
36
- let current = data.value as any
37
-
38
- for (let i = 0; i < keys.length; i++) {
39
- const key = keys[i]
40
- if (!current || typeof current !== 'object' || !(key in current)) {
41
- return ''
42
- }
43
- current = current[key]
44
- }
45
- return current ?? ''
46
- }
34
+ const getFieldData = (path?: string) => getNestedValue(data.value, path, '')
47
35
 
48
36
  const updateField = (path: string, value: any) => {
49
37
  const keys = path.split(/[.[]/)
@@ -180,3 +180,18 @@ export function pathKeyToURL(pathKey?: string) {
180
180
  }
181
181
  return `${fileBaseUrl}/${pathKey}`
182
182
  }
183
+
184
+ export function getNestedValue(obj: any, path?: string, defaultValue: any = undefined): any {
185
+ if (!path) return obj
186
+ const keys = path.split(/[.[]/)
187
+ let current = obj
188
+
189
+ for (const key of keys) {
190
+ if (!current || typeof current !== 'object' || !(key in current)) {
191
+ return defaultValue
192
+ }
193
+ current = current[key]
194
+ }
195
+
196
+ return current ?? defaultValue
197
+ }