@bagelink/vue 0.0.863 → 0.0.869

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.
@@ -1,4 +1,4 @@
1
- import { useModal } from '@bagelink/vue'
1
+ let modal: any
2
2
 
3
3
  export function applyFormatting(command: string, value: string = '') {
4
4
  const selection = window.getSelection()
@@ -7,6 +7,9 @@ export function applyFormatting(command: string, value: string = '') {
7
7
  const span = document.createElement('span')
8
8
 
9
9
  switch (command) {
10
+ case 'formatBlock':
11
+ formatBlock(value)
12
+ break
10
13
  case 'bold':
11
14
  span.style.fontWeight = 'bold'
12
15
  break
@@ -73,7 +76,7 @@ export function applyFormatting(command: string, value: string = '') {
73
76
  case 'decreaseFontSize':
74
77
  decreaseFontSize()
75
78
  break
76
- case 'insertLink':
79
+ case 'link':
77
80
  insertLink()
78
81
  break
79
82
  case 'image':
@@ -91,6 +94,16 @@ export function applyFormatting(command: string, value: string = '') {
91
94
  }
92
95
  }
93
96
 
97
+ function formatBlock(value: string) {
98
+ if (!value) return
99
+ const selection = window.getSelection()
100
+ const range = selection?.getRangeAt(0)
101
+ if (!range) return
102
+ range.selectNodeContents(range.startContainer)
103
+ const el = document.createElement(value)
104
+ range.surroundContents(el)
105
+ }
106
+
94
107
  function increaseFontSize() {
95
108
  const selection = window.getSelection()
96
109
  if (selection && selection.rangeCount > 0) {
@@ -104,7 +117,15 @@ function increaseFontSize() {
104
117
  }
105
118
 
106
119
  function decreaseFontSize() {
107
- // TODO:
120
+ const selection = window.getSelection()
121
+ if (selection && selection.rangeCount > 0) {
122
+ const range = selection.getRangeAt(0)
123
+ const span = document.createElement('span')
124
+ const currentFontSize = Number.parseInt((range.startContainer.parentNode as HTMLElement).style.fontSize || '16', 10)
125
+ const newFontSize = currentFontSize - 2
126
+ span.style.fontSize = `${newFontSize}px`
127
+ range.surroundContents(span)
128
+ }
108
129
  }
109
130
 
110
131
  function removeFormatting() {
@@ -142,36 +163,40 @@ function insertList(type: string) {
142
163
  }
143
164
 
144
165
  export function insertLink() {
145
- const url = prompt('Enter the URL:')
146
- if (url) {
147
- const selection = window.getSelection()
148
- if (selection && selection.rangeCount > 0) {
149
- const range = selection.getRangeAt(0)
166
+ const selection = window.getSelection()
167
+ if (!selection || selection.rangeCount < 1) return
168
+ const range = selection.getRangeAt(0)
169
+ modal.showModalForm({ title: 'Insert Link', schema: [
170
+ { id: 'url', $el: 'text', label: 'URL' },
171
+ { id: 'openInNewTab', $el: 'check', label: 'Open in new tab' },
172
+ ], onSubmit: (data: any) => {
173
+ const { url, openInNewTab } = data
174
+ if (url) {
150
175
  const anchor = document.createElement('a')
151
176
  anchor.href = url
152
177
  range.surroundContents(anchor)
178
+ if (openInNewTab) anchor.target = '_blank'
153
179
  }
154
- }
180
+ } })
155
181
  }
156
182
 
157
183
  export function insertImage() {
158
- console.log('image')
159
- const { showModalForm } = useModal()
160
- console.log(showModalForm)
161
- showModalForm({ title: 'Upload Image', schema: [{ id: 'image', type: 'file' }], onSubmit: (_data) => {
162
- //
184
+ const selection = window.getSelection()
185
+ if (!selection || selection.rangeCount < 1) return
186
+ const range = selection.getRangeAt(0)
187
+ modal.showModalForm({ title: 'Upload Image', schema: [
188
+ { id: 'src', $el: 'file', attrs: { bindkey: 'url' } },
189
+ { id: 'alt', $el: 'text', label: 'Alt Text' },
190
+ ], onSubmit: (_data: any) => {
191
+ const { src } = _data
192
+ if (src) {
193
+ const img = document.createElement('img')
194
+ img.src = src
195
+ img.alt = _data.alt
196
+ range.deleteContents()
197
+ range.insertNode(img)
198
+ }
163
199
  } })
164
- // const url = prompt('Enter the image URL:')
165
- // if (url) {
166
- // const selection = window.getSelection()
167
- // if (selection && selection.rangeCount > 0) {
168
- // const range = selection.getRangeAt(0)
169
- // const img = document.createElement('img')
170
- // img.src = url
171
- // range.deleteContents()
172
- // range.insertNode(img)
173
- // }
174
- // }
175
200
  }
176
201
 
177
202
  export function createTable() {
@@ -200,3 +225,22 @@ export function createTable() {
200
225
  }
201
226
  }
202
227
  }
228
+
229
+ function clearFormatting() {
230
+ const selection = window.getSelection()
231
+ const range = selection?.getRangeAt(0)
232
+ if (!range) return
233
+ range.selectNodeContents(range.startContainer)
234
+ const contents = range.extractContents()
235
+ const text = contents.textContent
236
+ if (text !== null && text !== '') {
237
+ range.deleteContents()
238
+ const textNode = document.createTextNode(`${text}`)
239
+ setTimeout(() => { range.insertNode(textNode) }, 1)
240
+ }
241
+ }
242
+
243
+ export function useFormatting(modalInstance?: any) {
244
+ modal = modalInstance
245
+ return { applyFormatting, clearFormatting }
246
+ }
@@ -1,14 +1,19 @@
1
1
  <script lang="ts" setup>
2
2
  import type { ToolbarConfig } from './richtext-types'
3
+ import { useModal } from '@bagelink/vue'
3
4
  import { onMounted, watch } from 'vue'
4
- import { applyFormatting } from './formatting'
5
+ import { useFormatting } from './formatting'
5
6
  import Toolbar from './Toolbar.vue'
6
7
 
7
8
  const props = defineProps<{ modelValue: string, toolbarConfig?: ToolbarConfig }>()
8
9
  const emit = defineEmits(['update:modelValue'])
9
10
 
11
+ const modal = useModal()
12
+ const { applyFormatting, clearFormatting } = useFormatting(modal)
13
+
10
14
  const editableContent = $ref<HTMLElement | undefined>()
11
15
  const defaultConfig: ToolbarConfig = [
16
+ 'formatBlock',
12
17
  'bold',
13
18
  'italic',
14
19
  'underline',
@@ -47,36 +52,37 @@ function updateContent() {
47
52
  emit('update:modelValue', contentHtml)
48
53
  }
49
54
 
55
+ function updateToolbarHighlight() {
56
+ if (document.getSelection()) {
57
+ const selection = document.getSelection()
58
+ const range = selection?.rangeCount ? selection.getRangeAt(0) : null
59
+ const container = range?.commonAncestorContainer as HTMLElement
60
+ if (container) {
61
+ let currentElement = container.nodeType === 3 ? container.parentElement : container
62
+ while (currentElement && currentElement !== editableContent) {
63
+ if (['H1', 'H2', 'H3'].includes(currentElement.tagName)) {
64
+ // emit('action', { type: currentElement.tagName.toLowerCase() }); // Emit the tag to highlight button
65
+ break
66
+ }
67
+ currentElement = currentElement.parentElement
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ document.addEventListener('selectionchange', updateToolbarHighlight)
74
+
75
+ type ToolbarAction = 'link' | 'image' | 'table' | 'youtube' | 'fontSize' | 'fontFamily' | 'textColor' | 'backgroundColor' | 'bold' | 'italic' | 'underline' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'alignJustify' | 'orderedList' | 'unorderedList' | 'indent' | 'outdent' | 'blockquote' | 'codeBlock' | 'splitView' | 'codeView' | 'clear' | 'fullScreen'
76
+
50
77
  function handleToolbarAction(action: string, value?: string) {
51
78
  if (!editableContent) return
52
79
  if (['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'].includes(action))
53
80
  value = action.replace('align', '').toLowerCase()
54
- switch (action) {
55
- case 'orderedList':
56
- applyFormatting('insertOrderedList')
57
- break
58
- case 'unorderedList':
59
- applyFormatting('insertUnorderedList')
60
- break
61
- case 'blockquote':
62
- applyFormatting('formatBlock', '<blockquote>')
63
- break
64
- case 'codeBlock':
65
- applyFormatting('formatBlock', '<pre>')
66
- break
67
- case 'splitView':
68
- isSplitView = !isSplitView
69
- break
70
- case 'codeView':
71
- isCodeView = !isCodeView
72
- break
73
- case 'fullScreen':
74
- toggleFullScreen()
75
- break
76
- default:
77
- applyFormatting(action, value)
78
- break
79
- }
81
+ if (action === 'splitView') isSplitView = !isSplitView
82
+ else if (action === 'codeView') isCodeView = !isCodeView
83
+ else if (action === 'fullScreen') toggleFullScreen()
84
+ else if (action === 'clear') clearFormatting()
85
+ else applyFormatting(action, value)
80
86
  updateContent()
81
87
  }
82
88
 
@@ -131,13 +137,17 @@ function handleKeyDown(event: KeyboardEvent) {
131
137
  }
132
138
  }
133
139
  }
140
+ function logInput(e: InputEvent) {
141
+ const target = e.target as HTMLElement
142
+ contentHtml = target.textContent || ''
143
+ }
134
144
  </script>
135
145
 
136
146
  <template>
137
- <div class="rich-text-editor rounded pt-05 px-1 pb-1">
147
+ <div class="rich-text-editor rounded pt-05 px-05 pb-1">
138
148
  <Toolbar :config @action="handleToolbarAction" />
139
149
  <div class="editor-container flex flex-stretch gap-1 m_column">
140
- <div class="content-area radius-05 p-1 shadow-light w-100 grid">
150
+ <div class="content-area radius-05 p-1 w-100 grid">
141
151
  <textarea v-if="isCodeView" v-model="contentHtml" @input="updateContent" />
142
152
  <div
143
153
  v-else
@@ -151,7 +161,13 @@ function handleKeyDown(event: KeyboardEvent) {
151
161
  @keydown="handleKeyDown"
152
162
  />
153
163
  </div>
154
- <code v-if="isSplitView" class="preview-area w-100 radius-05 p-1">{{ contentHtml }}</code>
164
+ <code
165
+ v-if="isSplitView"
166
+ contenteditable="true"
167
+ class="preview-area w-100 radius-05 p-1"
168
+ @input="(e) => logInput(e as InputEvent)"
169
+ v-text="contentHtml"
170
+ />
155
171
  </div>
156
172
  </div>
157
173
  </template>
@@ -24,5 +24,6 @@ export type ToolbarConfigOption = 'bold'
24
24
  | 'youtube'
25
25
  | 'clear'
26
26
  | 'fullScreen'
27
+ | 'formatBlock'
27
28
 
28
29
  export type ToolbarConfig = ToolbarConfigOption[]
@@ -10,8 +10,8 @@ export { default as PasswordInput } from './PasswordInput.vue'
10
10
  export { default as RadioGroup } from './RadioGroup.vue'
11
11
  export { default as RadioPillsInput } from './RadioPillsInput.vue'
12
12
  export { default as RangeInput } from './RangeInput.vue'
13
- export { default as RichText } from './RichText.vue'
14
- export { default as RichText2 } from './RichText2/index.vue'
13
+ export { default as RichText2 } from './RichText.vue'
14
+ export { default as RichText } from './RichText2/index.vue'
15
15
  export { default as SelectInput } from './SelectInput.vue'
16
16
  export { default as SignaturePad } from './SignaturePad.vue'
17
17
  export { default as TableField } from './TableField.vue'
@@ -51,6 +51,10 @@
51
51
  border-radius: 100%;
52
52
  }
53
53
 
54
+ .aspect-ratio-1 {
55
+ aspect-ratio: 1;
56
+ }
57
+
54
58
  .flex-center {
55
59
  justify-content: center;
56
60
  align-items: center;
@@ -72,6 +72,10 @@
72
72
  border-radius: 100%;
73
73
  }
74
74
 
75
+ .m_aspect-ratio-1 {
76
+ aspect-ratio: 1;
77
+ }
78
+
75
79
  .m_flex-center {
76
80
  justify-items: center;
77
81
  align-items: center;