@bagelink/vue 0.0.544 → 0.0.552

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 (35) hide show
  1. package/dist/components/TableSchema.vue.d.ts +7 -16
  2. package/dist/components/TableSchema.vue.d.ts.map +1 -1
  3. package/dist/components/form/BglField.vue.d.ts.map +1 -1
  4. package/dist/components/form/inputs/RichText2/Toolbar.vue.d.ts +22 -0
  5. package/dist/components/form/inputs/RichText2/Toolbar.vue.d.ts.map +1 -0
  6. package/dist/components/form/inputs/RichText2/formatting.d.ts +5 -0
  7. package/dist/components/form/inputs/RichText2/formatting.d.ts.map +1 -0
  8. package/dist/components/form/inputs/RichText2/index.vue.d.ts +24 -0
  9. package/dist/components/form/inputs/RichText2/index.vue.d.ts.map +1 -0
  10. package/dist/components/form/inputs/RichText2/richtext-types.d.ts +3 -0
  11. package/dist/components/form/inputs/RichText2/richtext-types.d.ts.map +1 -0
  12. package/dist/components/form/inputs/index.d.ts +1 -0
  13. package/dist/components/form/inputs/index.d.ts.map +1 -1
  14. package/dist/index.cjs +706 -278
  15. package/dist/index.mjs +706 -278
  16. package/dist/style.css +448 -119
  17. package/dist/types/BagelForm.d.ts +5 -4
  18. package/dist/types/BagelForm.d.ts.map +1 -1
  19. package/package.json +1 -1
  20. package/src/components/Btn.vue +1 -1
  21. package/src/components/DataPreview.vue +3 -3
  22. package/src/components/TableSchema.vue +10 -10
  23. package/src/components/form/BglField.vue +19 -2
  24. package/src/components/form/inputs/RichText2/Toolbar.vue +106 -0
  25. package/src/components/form/inputs/RichText2/formatting.ts +194 -0
  26. package/src/components/form/inputs/RichText2/index.vue +204 -0
  27. package/src/components/form/inputs/RichText2/richtext-types.ts +25 -0
  28. package/src/components/form/inputs/index.ts +1 -0
  29. package/src/components/formkit/FileUploader.vue +1 -1
  30. package/src/styles/appearance.css +16 -16
  31. package/src/styles/layout.css +113 -6
  32. package/src/styles/mobilLayout.css +124 -12
  33. package/src/styles/text.css +78 -12
  34. package/src/styles/theme.css +1 -0
  35. package/src/types/BagelForm.ts +15 -1
@@ -5,6 +5,10 @@ export type AttributeFn<T = Record<string, any>> = (field: any, row: T) => Attri
5
5
  export interface Attributes<T = any> {
6
6
  [key: string]: AttributeValue | AttributeFn<T>;
7
7
  }
8
+ export type BagelFieldOptions = (string | ({
9
+ label?: string;
10
+ value: string | number;
11
+ } | string | number | Record<string, any>)[] | ((val: any, rowData?: Record<string, any>) => void));
8
12
  export interface BaseBagelField<T = Record<string, any>> {
9
13
  '$el'?: any;
10
14
  'id'?: string;
@@ -16,10 +20,7 @@ export interface BaseBagelField<T = Record<string, any>> {
16
20
  'required'?: boolean;
17
21
  'disabled'?: boolean;
18
22
  'helptext'?: string;
19
- 'options'?: string | ({
20
- label?: string;
21
- value: string | number;
22
- } | string | number | Record<string, any>)[] | ((val: any, rowData?: Record<string, any>) => void);
23
+ 'options'?: BagelFieldOptions;
23
24
  'defaultValue'?: any;
24
25
  'transform'?: (val?: any, rowData?: Record<string, any>) => any;
25
26
  'onUpdate'?: (val: any, rowData?: Record<string, any>) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"BagelForm.d.ts","sourceRoot":"","sources":["../../src/types/BagelForm.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BagelForm.d.ts","sourceRoot":"","sources":["../../src/types/BagelForm.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "0.0.544",
4
+ "version": "0.0.552",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Neveh Allon",
@@ -207,7 +207,7 @@ a {
207
207
 
208
208
  .bgl_btn.bgl_flatBtn:active,
209
209
  .bgl_btn-icon.bgl_flatBtn:active {
210
- background: var(--bgl-gray);
210
+ background: var(--bgl-gray-40);
211
211
  filter: var(--bgl-hover-filter);
212
212
  }
213
213
 
@@ -15,12 +15,12 @@ const props = defineProps<{
15
15
 
16
16
  const itemData = defineModel<Record<string, any>>('data', { default: {} })
17
17
  const computedSchema = $computed(() => {
18
- console.log(itemData.value)
18
+ // console.log(itemData.value)
19
19
  const schema = Object.keys(itemData.value)
20
20
  if (props.showFields && props.showFields.length) {
21
21
  return schema.filter((field: any) => props.showFields?.includes(field))
22
22
  }
23
- console.log(schema)
23
+ // console.log(schema)
24
24
  return schema
25
25
  })
26
26
  </script>
@@ -33,7 +33,7 @@ const computedSchema = $computed(() => {
33
33
  <div class="key">
34
34
  {{ field?.label || keyToLabel(field.id) }}
35
35
  </div>
36
- <BglField v-model="itemData" label="" style="width: min-content; min-width: 140px" :field="field" />
36
+ <BglField v-model="itemData" label="" style="width: min-content; min-width: 140px" :field />
37
37
  </div>
38
38
  </template>
39
39
  <div v-if="!schema?.length">
@@ -1,28 +1,28 @@
1
1
  <script setup lang="ts">
2
- import { useSlots } from 'vue'
3
2
  import {
4
3
  BglField,
5
4
  type BglFormSchemaT,
6
5
  MaterialIcon,
7
6
  keyToLabel,
8
7
  } from '@bagelink/vue'
8
+ import { useSlots } from 'vue'
9
9
 
10
10
  const props = defineProps<{
11
- selecteItems?: string[]
11
+ selectedItems?: string[]
12
12
  data: any[]
13
13
  schema?: BglFormSchemaT | (() => BglFormSchemaT)
14
14
  showFields?: string[]
15
15
  }>()
16
- const emit = defineEmits(['update:selecteItems', 'orderBy', 'select'])
16
+ const emit = defineEmits(['update:selectedItems', 'orderBy', 'select'])
17
17
  const slots = useSlots()
18
18
  const loading = $ref(true)
19
19
 
20
- let selecteItems = $ref<string[]>(props.selecteItems || [])
20
+ let selectedItems = $ref<string[]>(props.selectedItems || [])
21
21
  let selected = $computed({
22
- get: () => selecteItems,
22
+ get: () => selectedItems,
23
23
  set: (value: string[]) => {
24
- selecteItems = value
25
- emit('update:selecteItems', value)
24
+ selectedItems = value
25
+ emit('update:selectedItems', value)
26
26
  },
27
27
  })
28
28
 
@@ -160,13 +160,13 @@ function sort(fieldname: string) {
160
160
  <slot
161
161
  v-if="field.id && slots[field.id]"
162
162
  :name="field.id"
163
- :row="row"
164
- :field="field"
163
+ :row
164
+ :field
165
165
  />
166
166
  <div v-else>
167
167
  <BglField
168
168
  class="embedded-field"
169
- :field="field"
169
+ :field
170
170
  :modelValue="row"
171
171
  label=""
172
172
  />
@@ -49,7 +49,7 @@ const fieldData = $computed({
49
49
  },
50
50
  get: () => {
51
51
  if (props.field.id) return props.modelValue[props.field.id]
52
- return props.field.defaultValue || ''
52
+ return props.field.defaultValue ?? ''
53
53
  },
54
54
  })
55
55
 
@@ -62,6 +62,23 @@ const vIf = $computed(() => {
62
62
  }
63
63
  return true
64
64
  })
65
+
66
+ const badTransform = /undefined|NaN|Invalid Date/
67
+ const computedFieldData = $computed(
68
+ () => {
69
+ // region AVOID_TRANSFORMING_DATA_MULTIPLE_TIMES
70
+ // const alreadyTransformed = props.modelValue[props.field.id ?? ''] === fieldData
71
+ // if (alreadyTransformed) return fieldData
72
+ // return props.field.transform?.(fieldData, props.modelValue)
73
+ // endregion AVOID_TRANSFORMING_DATA_MULTIPLE_TIMES
74
+
75
+ // region CHECK_FOR_BAD_TRANSFORMS
76
+ const transformedData = props.field.transform?.(fieldData, props.modelValue)
77
+ if (badTransform.test(transformedData)) return fieldData
78
+ return transformedData
79
+ // endregion CHECK_FOR_BAD_TRANSFORMS
80
+ }
81
+ )
65
82
  </script>
66
83
 
67
84
  <template>
@@ -83,7 +100,7 @@ const vIf = $computed(() => {
83
100
  :helptext="field.helptext"
84
101
  @update:modelValue="($event: any) => field?.onUpdate?.($event, formData)"
85
102
  >
86
- {{ field.transform?.(fieldData, modelValue) || fieldData || '' }}
103
+ {{ computedFieldData }}
87
104
  <BglField
88
105
  v-for="(child, ii) in field.children"
89
106
  :key="child.id || ii"
@@ -0,0 +1,106 @@
1
+ <script lang="ts" setup>
2
+ import { Btn, type MaterialIcons } from '@bagelink/vue'
3
+ import type { ToolbarConfig, ToolbarConfigOption } from './richtext-types'
4
+
5
+ defineProps<{ config: ToolbarConfig }>()
6
+ const emit = defineEmits(['action'])
7
+
8
+ const colorPicker = $ref<HTMLInputElement | null>(null)
9
+ const fontSizeInput = $ref<HTMLInputElement | null>(null)
10
+
11
+ interface toolbarOption {
12
+ name: ToolbarConfigOption
13
+ icon: MaterialIcons
14
+ }
15
+ const toolbarOptions: toolbarOption[] = [
16
+ { name: 'bold', icon: 'format_bold' },
17
+ { name: 'italic', icon: 'format_italic' },
18
+ { name: 'underline', icon: 'format_underlined' },
19
+ { name: 'fontSize', icon: 'format_size' },
20
+ { name: 'fontFamily', icon: 'font_download' },
21
+ { name: 'textColor', icon: 'text_format' },
22
+ { name: 'backgroundColor', icon: 'format_color_fill' },
23
+ { name: 'alignLeft', icon: 'format_align_left' },
24
+ { name: 'alignCenter', icon: 'format_align_center' },
25
+ { name: 'alignRight', icon: 'format_align_right' },
26
+ { name: 'alignJustify', icon: 'format_align_justify' },
27
+ { name: 'orderedList', icon: 'format_list_numbered' },
28
+ { name: 'unorderedList', icon: 'format_list_bulleted' },
29
+ { name: 'indent', icon: 'format_indent_increase' },
30
+ { name: 'outdent', icon: 'format_indent_decrease' },
31
+ { name: 'link', icon: 'link' },
32
+ { name: 'image', icon: 'image' },
33
+ { name: 'table', icon: 'table_chart' },
34
+ { name: 'blockquote', icon: 'format_quote' },
35
+ { name: 'codeBlock', icon: 'code' },
36
+ { name: 'splitView', icon: 'splitscreen_right' },
37
+ { name: 'codeView', icon: 'code' },
38
+ ]
39
+
40
+ function handleColorPick(type: 'textColor' | 'backgroundColor') {
41
+ const color = colorPicker?.value
42
+ if (color) {
43
+ emit('action', type, color)
44
+ }
45
+ }
46
+
47
+ function handleFontSizeChange() {
48
+ const size = fontSizeInput?.value
49
+ if (size) {
50
+ emit('action', 'fontSize', size)
51
+ }
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <div class="toolbar" role="toolbar">
57
+ <Btn
58
+ v-for="action in toolbarOptions.filter(t => config.includes(t.name))"
59
+ :key="action.name"
60
+ v-tooltip="action.name"
61
+ thin
62
+ color="light"
63
+ :aria-label="action.name"
64
+ :icon="action.icon"
65
+ @click="emit('action', action.name)"
66
+ />
67
+ <input
68
+ v-if="config.includes('textColor') || config.includes('backgroundColor')"
69
+ ref="colorPicker"
70
+ type="color"
71
+ @change="handleColorPick(config.includes('textColor') ? 'textColor' : 'backgroundColor')"
72
+ >
73
+ <input
74
+ v-if="config.includes('fontSize')"
75
+ ref="fontSizeInput"
76
+ type="number"
77
+ min="1"
78
+ max="7"
79
+ @change="handleFontSizeChange"
80
+ >
81
+ </div>
82
+ </template>
83
+
84
+ <style scoped>
85
+ .toolbar {
86
+ display: flex;
87
+ flex-wrap: wrap;
88
+ gap: 5px;
89
+ padding: 10px;
90
+ background-color: #f0f0f0;
91
+ border-bottom: 1px solid #ccc;
92
+ }
93
+
94
+ button:hover {
95
+ background-color: #e0e0e0;
96
+ }
97
+
98
+ input[type="color"],
99
+ input[type="number"] {
100
+ width: 40px;
101
+ height: 30px;
102
+ padding: 0;
103
+ border: 1px solid #ccc;
104
+ border-radius: 3px;
105
+ }
106
+ </style>
@@ -0,0 +1,194 @@
1
+ export function applyFormatting(command: string, value: string = '') {
2
+ const selection = window.getSelection()
3
+ if (selection && selection.rangeCount > 0) {
4
+ const range = selection.getRangeAt(0)
5
+ const span = document.createElement('span')
6
+
7
+ switch (command) {
8
+ case 'bold':
9
+ span.style.fontWeight = 'bold'
10
+ break
11
+ case 'italic':
12
+ span.style.fontStyle = 'italic'
13
+ break
14
+ case 'underline':
15
+ span.style.textDecoration = 'underline'
16
+ break
17
+ case 'strikeThrough':
18
+ span.style.textDecoration = 'line-through'
19
+ break
20
+ case 'fontSize':
21
+ span.style.fontSize = value
22
+ break
23
+ case 'foreColor':
24
+ span.style.color = value
25
+ break
26
+ case 'backColor':
27
+ span.style.backgroundColor = value
28
+ break
29
+ case 'justifyLeft':
30
+ span.style.textAlign = 'left'
31
+ break
32
+ case 'justifyCenter':
33
+ span.style.textAlign = 'center'
34
+ break
35
+ case 'justifyRight':
36
+ span.style.textAlign = 'right'
37
+ break
38
+ case 'justifyFull':
39
+ span.style.textAlign = 'justify'
40
+ break
41
+ case 'insertOrderedList':
42
+ insertList('ol')
43
+ return
44
+ case 'insertUnorderedList':
45
+ insertList('ul')
46
+ return
47
+ case 'indent':
48
+ span.style.marginLeft = '40px'
49
+ break
50
+ case 'outdent':
51
+ span.style.marginLeft = '-40px'
52
+ break
53
+ case 'removeFormatting':
54
+ removeFormatting()
55
+ break
56
+ case 'insertHorizontalRule':
57
+ insertHorizontalRule()
58
+ break
59
+ case 'subscript':
60
+ span.style.verticalAlign = 'sub'
61
+ break
62
+ case 'superscript':
63
+ span.style.verticalAlign = 'super'
64
+ break
65
+ case 'fontName':
66
+ span.style.fontFamily = value
67
+ break
68
+ case 'increaseFontSize':
69
+ increaseFontSize()
70
+ break
71
+ case 'decreaseFontSize':
72
+ decreaseFontSize()
73
+ break
74
+ case 'insertLink':
75
+ insertLink()
76
+ break
77
+ case 'insertImage':
78
+ insertImage()
79
+ break
80
+ case 'createTable':
81
+ createTable()
82
+ break
83
+ default:
84
+ console.warn(`Command ${command} is not supported`)
85
+ return
86
+ }
87
+
88
+ range.surroundContents(span)
89
+ }
90
+ }
91
+
92
+ function increaseFontSize() {
93
+ const selection = window.getSelection()
94
+ if (selection && selection.rangeCount > 0) {
95
+ const range = selection.getRangeAt(0)
96
+ const span = document.createElement('span')
97
+ const currentFontSize = Number.parseInt((range.startContainer.parentNode as HTMLElement).style.fontSize || '16', 10)
98
+ const newFontSize = currentFontSize + 2
99
+ span.style.fontSize = `${newFontSize}px`
100
+ range.surroundContents(span)
101
+ }
102
+ }
103
+
104
+ function decreaseFontSize() {
105
+
106
+ }
107
+
108
+ function removeFormatting() {
109
+ const selection = window.getSelection()
110
+ if (selection && selection.rangeCount > 0) {
111
+ const range = selection.getRangeAt(0)
112
+ const span = document.createElement('span')
113
+ span.textContent = range.toString()
114
+ range.deleteContents()
115
+ range.insertNode(span)
116
+ }
117
+ }
118
+
119
+ function insertHorizontalRule() {
120
+ const selection = window.getSelection()
121
+ if (selection && selection.rangeCount > 0) {
122
+ const range = selection.getRangeAt(0)
123
+ const hr = document.createElement('hr')
124
+ range.deleteContents()
125
+ range.insertNode(hr)
126
+ }
127
+ }
128
+
129
+ function insertList(type: string) {
130
+ const selection = window.getSelection()
131
+ if (selection && selection.rangeCount > 0) {
132
+ const range = selection.getRangeAt(0)
133
+ const list = document.createElement(type)
134
+ const listItem = document.createElement('li')
135
+ listItem.textContent = range.toString()
136
+ list.appendChild(listItem)
137
+ range.deleteContents()
138
+ range.insertNode(list)
139
+ }
140
+ }
141
+
142
+ export function insertLink() {
143
+ const url = prompt('Enter the URL:')
144
+ if (url) {
145
+ const selection = window.getSelection()
146
+ if (selection && selection.rangeCount > 0) {
147
+ const range = selection.getRangeAt(0)
148
+ const anchor = document.createElement('a')
149
+ anchor.href = url
150
+ range.surroundContents(anchor)
151
+ }
152
+ }
153
+ }
154
+
155
+ export function insertImage() {
156
+ const url = prompt('Enter the image URL:')
157
+ if (url) {
158
+ const selection = window.getSelection()
159
+ if (selection && selection.rangeCount > 0) {
160
+ const range = selection.getRangeAt(0)
161
+ const img = document.createElement('img')
162
+ img.src = url
163
+ range.deleteContents()
164
+ range.insertNode(img)
165
+ }
166
+ }
167
+ }
168
+
169
+ export function createTable() {
170
+ const rows = prompt('Enter the number of rows:')
171
+ const cols = prompt('Enter the number of columns:')
172
+ if (rows && cols) {
173
+ const table = document.createElement('table')
174
+ table.style.border = '1px solid black'
175
+ table.style.borderCollapse = 'collapse'
176
+
177
+ for (let i = 0; i < Number.parseInt(rows); i++) {
178
+ const row = table.insertRow()
179
+ for (let j = 0; j < Number.parseInt(cols); j++) {
180
+ const cell = row.insertCell()
181
+ cell.style.border = '1px solid black'
182
+ cell.style.padding = '5px'
183
+ cell.textContent = `Cell ${i + 1},${j + 1}`
184
+ }
185
+ }
186
+
187
+ const selection = window.getSelection()
188
+ if (selection && selection.rangeCount > 0) {
189
+ const range = selection.getRangeAt(0)
190
+ range.deleteContents()
191
+ range.insertNode(table)
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,204 @@
1
+ <script lang="ts" setup>
2
+ import { onMounted, watch } from 'vue'
3
+ import Toolbar from './Toolbar.vue'
4
+ import type { ToolbarConfig } from './richtext-types'
5
+ import { applyFormatting } from './formatting'
6
+
7
+ const props = defineProps<{ modelValue: string, toolbarConfig?: ToolbarConfig }>()
8
+ const emit = defineEmits(['update:modelValue'])
9
+
10
+ const editableContent = $ref<HTMLElement | null>(null)
11
+ const defaultConfig: ToolbarConfig = [
12
+ 'bold',
13
+ 'italic',
14
+ 'underline',
15
+ 'fontSize',
16
+ 'fontFamily',
17
+ 'textColor',
18
+ 'backgroundColor',
19
+ 'alignLeft',
20
+ 'alignCenter',
21
+ 'alignRight',
22
+ 'alignJustify',
23
+ 'orderedList',
24
+ 'unorderedList',
25
+ 'indent',
26
+ 'outdent',
27
+ 'link',
28
+ 'image',
29
+ 'table',
30
+ 'blockquote',
31
+ 'codeBlock',
32
+ 'splitView',
33
+ 'codeView'
34
+ ]
35
+ const config = $ref<ToolbarConfig>(props.toolbarConfig || defaultConfig)
36
+ let contentHtml = $ref(props.modelValue)
37
+ let isCodeView = $ref(false)
38
+ let isSplitView = $ref(false)
39
+
40
+ function updateContent() {
41
+ if (!isCodeView) {
42
+ contentHtml = editableContent?.innerHTML ?? ''
43
+ }
44
+ emit('update:modelValue', contentHtml)
45
+ }
46
+
47
+ function handleToolbarAction(action: string, value?: string) {
48
+ if (!editableContent) return
49
+ if (['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'].includes(action))
50
+ value = action.replace('align', '').toLowerCase()
51
+ switch (action) {
52
+ case 'orderedList':
53
+ applyFormatting('insertOrderedList')
54
+ break
55
+ case 'unorderedList':
56
+ applyFormatting('insertUnorderedList')
57
+ break
58
+ case 'blockquote':
59
+ applyFormatting('formatBlock', '<blockquote>')
60
+ break
61
+ case 'codeBlock':
62
+ applyFormatting('formatBlock', '<pre>')
63
+ break
64
+ case 'splitView':
65
+ isSplitView = !isSplitView
66
+ break
67
+ case 'codeView':
68
+ isCodeView = !isCodeView
69
+ break
70
+ default:
71
+ applyFormatting(action, value)
72
+ break
73
+ }
74
+ updateContent()
75
+ }
76
+
77
+ watch(() => props.modelValue, (newValue) => {
78
+ if (newValue !== contentHtml) {
79
+ contentHtml = newValue
80
+ if (editableContent) {
81
+ editableContent.innerHTML = newValue
82
+ }
83
+ }
84
+ })
85
+
86
+ onMounted(() => {
87
+ if (editableContent) {
88
+ editableContent.innerHTML = contentHtml
89
+ }
90
+ })
91
+
92
+ // Keyboard shortcuts
93
+ function handleKeyDown(event: KeyboardEvent) {
94
+ if (event.ctrlKey || event.metaKey) {
95
+ switch (event.key.toLowerCase()) {
96
+ case 'b':
97
+ event.preventDefault()
98
+ handleToolbarAction('bold')
99
+ break
100
+ case 'i':
101
+ event.preventDefault()
102
+ handleToolbarAction('italic')
103
+ break
104
+ case 'u':
105
+ event.preventDefault()
106
+ handleToolbarAction('underline')
107
+ break
108
+ case 'k':
109
+ event.preventDefault()
110
+ handleToolbarAction('link')
111
+ break
112
+ case 'l':
113
+ event.preventDefault()
114
+ handleToolbarAction('orderedList')
115
+ break
116
+ default:
117
+ break
118
+ }
119
+ }
120
+ }
121
+ </script>
122
+
123
+ <template>
124
+ <div class="rich-text-editor">
125
+ <Toolbar :config @action="handleToolbarAction" />
126
+ <div class="editor-container" :class="{ 'split-view': isSplitView }">
127
+ <div class="content-area" :class="{ 'code-view': isCodeView }">
128
+ <textarea v-if="isCodeView" v-model="contentHtml" @input="updateContent" />
129
+ <div
130
+ v-else
131
+ ref="editableContent"
132
+ contenteditable="true"
133
+ role="textbox"
134
+ aria-multiline="true"
135
+ tabindex="0"
136
+ @input="updateContent"
137
+ @keydown="handleKeyDown"
138
+ />
139
+ </div>
140
+ <div v-if="isSplitView" class="preview-area" v-html="contentHtml" />
141
+ </div>
142
+ </div>
143
+ </template>
144
+
145
+ <style scoped>
146
+ .rich-text-editor {
147
+ font-family: Arial, sans-serif;
148
+ max-width: 800px;
149
+ margin: 0 auto;
150
+ }
151
+
152
+ [contenteditable='true'] {
153
+ white-space: pre-wrap;
154
+ word-wrap: break-word;
155
+ outline: none;
156
+ border: none;
157
+ height: 100%;
158
+ }
159
+
160
+ .editor-container {
161
+ display: flex;
162
+ border: 1px solid #ccc;
163
+ border-radius: 4px;
164
+ overflow: hidden;
165
+ }
166
+
167
+ .content-area,
168
+ .preview-area {
169
+ flex: 1;
170
+ min-height: 300px;
171
+ padding: 10px;
172
+ overflow-y: auto;
173
+ }
174
+
175
+ .content-area:focus {
176
+ outline: none;
177
+ }
178
+
179
+ .split-view .content-area,
180
+ .split-view .preview-area {
181
+ width: 50%;
182
+ }
183
+
184
+ .preview-area {
185
+ background-color: #f9f9f9;
186
+ border-left: 1px solid #ccc;
187
+ }
188
+
189
+ @media (max-width: 768px) {
190
+ .split-view {
191
+ flex-direction: column;
192
+ }
193
+
194
+ .split-view .content-area,
195
+ .split-view .preview-area {
196
+ width: 100%;
197
+ }
198
+
199
+ .preview-area {
200
+ border-left: none;
201
+ border-top: 1px solid #ccc;
202
+ }
203
+ }
204
+ </style>