@bagelink/vue 0.0.984 → 0.0.988
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/DropDown.vue.d.ts +2 -2
- package/dist/components/DropDown.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/CodeEditor/CodeTypes.d.ts +197 -0
- package/dist/components/form/inputs/CodeEditor/CodeTypes.d.ts.map +1 -0
- package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts +60 -0
- package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/CodeEditor/format.d.ts +2 -0
- package/dist/components/form/inputs/CodeEditor/format.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/Toolbar.vue.d.ts +5 -3
- package/dist/components/form/inputs/RichText/Toolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/Toolbar.vue.d.ts +14 -0
- package/dist/components/form/inputs/RichText/components/Toolbar.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/components/gridBox.vue.d.ts +11 -0
- package/dist/components/form/inputs/RichText/components/gridBox.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +86 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +2 -0
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/config.d.ts +5 -0
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/index.vue.d.ts +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts +7 -0
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/media.d.ts +4 -0
- package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +4 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/table.d.ts +6 -0
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -0
- package/dist/components/form/inputs/SelectInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/index.d.ts +1 -0
- package/dist/components/form/inputs/index.d.ts.map +1 -1
- package/dist/editor-CUDRLdmS.js +4 -0
- package/dist/editor-Cu374vEW.cjs +4 -0
- package/dist/index.cjs +53544 -766
- package/dist/index.mjs +53545 -767
- package/dist/style.css +19779 -97
- package/package.json +2 -1
- package/src/components/Btn.vue +3 -0
- package/src/components/Dropdown.vue +1 -1
- package/src/components/form/inputs/CodeEditor/CodeTypes.ts +446 -0
- package/src/components/form/inputs/CodeEditor/Index.vue +440 -0
- package/src/components/form/inputs/CodeEditor/format.ts +98 -0
- package/src/components/form/inputs/CodeEditor/themes/brown-papersq.png +0 -0
- package/src/components/form/inputs/CodeEditor/themes/pojoaque.jpg +0 -0
- package/src/components/form/inputs/CodeEditor/themes/themes-base16.css +12809 -0
- package/src/components/form/inputs/CodeEditor/themes/themes.css +6740 -0
- package/src/components/form/inputs/RichText/components/Toolbar.vue +51 -0
- package/src/components/form/inputs/RichText/components/gridBox.vue +22 -0
- package/src/components/form/inputs/RichText/composables/useEditor.ts +208 -0
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +21 -0
- package/src/components/form/inputs/RichText/config.ts +73 -0
- package/src/components/form/inputs/RichText/editor.css +25 -0
- package/src/components/form/inputs/RichText/index.vue +81 -193
- package/src/components/form/inputs/RichText/richTextTypes.d.ts +77 -0
- package/src/components/form/inputs/RichText/utils/formatting.ts +98 -0
- package/src/components/form/inputs/RichText/utils/media.ts +42 -0
- package/src/components/form/inputs/RichText/utils/selection.ts +48 -0
- package/src/components/form/inputs/RichText/utils/table.ts +79 -0
- package/src/components/form/inputs/index.ts +1 -0
- package/src/components/form/inputs/RichText/Toolbar.vue +0 -87
- package/src/components/form/inputs/RichText/formatting.ts +0 -246
- package/src/components/form/inputs/RichText/richtext-types.ts +0 -29
|
@@ -1,253 +1,141 @@
|
|
|
1
|
-
<script lang="ts"
|
|
2
|
-
import type { ToolbarConfig } from './
|
|
3
|
-
import { useModal } from '@bagelink/vue'
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ToolbarConfig } from './richTextTypes'
|
|
3
|
+
import { useModal, CodeEditor } from '@bagelink/vue'
|
|
4
|
+
import { watch } from 'vue'
|
|
5
|
+
import Toolbar from './components/Toolbar.vue'
|
|
6
|
+
import { useEditor } from './composables/useEditor'
|
|
7
|
+
import { useEditorKeyboard } from './composables/useEditorKeyboard'
|
|
7
8
|
|
|
8
9
|
const props = defineProps<{ modelValue: string, toolbarConfig?: ToolbarConfig }>()
|
|
9
10
|
const emit = defineEmits(['update:modelValue'])
|
|
10
11
|
|
|
12
|
+
const iframe = $ref<HTMLIFrameElement>()
|
|
11
13
|
const modal = useModal()
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const editableContent = ref<HTMLElement>()
|
|
15
|
-
const defaultConfig: ToolbarConfig = [
|
|
16
|
-
'formatBlock',
|
|
17
|
-
'bold',
|
|
18
|
-
'italic',
|
|
19
|
-
'underline',
|
|
20
|
-
'fontSize',
|
|
21
|
-
'fontFamily',
|
|
22
|
-
'textColor',
|
|
23
|
-
'backgroundColor',
|
|
24
|
-
'alignLeft',
|
|
25
|
-
'alignCenter',
|
|
26
|
-
'alignRight',
|
|
27
|
-
'alignJustify',
|
|
28
|
-
'orderedList',
|
|
29
|
-
'unorderedList',
|
|
30
|
-
'indent',
|
|
31
|
-
'outdent',
|
|
32
|
-
'link',
|
|
33
|
-
'image',
|
|
34
|
-
'table',
|
|
35
|
-
'splitView',
|
|
36
|
-
'clear',
|
|
37
|
-
'fullScreen'
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
const config = ref<ToolbarConfig>(props.toolbarConfig || defaultConfig)
|
|
41
|
-
const contentHtml = ref(props.modelValue)
|
|
42
|
-
const isCodeView = ref(false)
|
|
43
|
-
const isSplitView = ref(false)
|
|
44
|
-
const selectedStyles = ref(new Set<string>())
|
|
45
|
-
|
|
46
|
-
function updateContent(source: 'html' | 'editor' = 'editor') {
|
|
47
|
-
if (source === 'editor' && !isCodeView.value) {
|
|
48
|
-
contentHtml.value = editableContent.value?.innerHTML ?? ''
|
|
49
|
-
}
|
|
50
|
-
emit('update:modelValue', contentHtml.value)
|
|
51
|
-
}
|
|
14
|
+
const editor = useEditor()
|
|
52
15
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
editableContent.value.innerHTML = target.value
|
|
16
|
+
async function initEditor() {
|
|
17
|
+
if (!iframe) {
|
|
18
|
+
setTimeout(initEditor, 100)
|
|
19
|
+
return
|
|
58
20
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
function updateToolbarHighlight() {
|
|
63
|
-
selectedStyles.value.clear()
|
|
64
|
-
const selection = window.getSelection()
|
|
65
|
-
if (!selection?.rangeCount) return
|
|
66
|
-
|
|
67
|
-
const range = selection.getRangeAt(0)
|
|
68
|
-
const element = range.commonAncestorContainer as HTMLElement
|
|
69
|
-
const parent = element.nodeType === 3 ? element.parentElement : element
|
|
21
|
+
editor.state.content = props.modelValue || ''
|
|
22
|
+
const doc = iframe.contentDocument || iframe.contentWindow?.document
|
|
23
|
+
if (!doc) return
|
|
70
24
|
|
|
71
|
-
|
|
25
|
+
doc.designMode = 'on'
|
|
26
|
+
doc.body.contentEditable = 'true'
|
|
72
27
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function handleToolbarAction(action: string, value?: string) {
|
|
79
|
-
if (!editableContent.value) return
|
|
28
|
+
const style = doc.createElement('style')
|
|
29
|
+
style.textContent = (await import('./editor.css?inline')).default
|
|
30
|
+
doc.head.appendChild(style)
|
|
80
31
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
32
|
+
editor.init(doc, modal)
|
|
33
|
+
useEditorKeyboard(doc, (action, value) => {
|
|
34
|
+
editor.handleToolbarAction(action, value)
|
|
35
|
+
})
|
|
84
36
|
|
|
85
|
-
|
|
86
|
-
case 'splitView':
|
|
87
|
-
isSplitView.value = !isSplitView.value
|
|
88
|
-
break
|
|
89
|
-
case 'codeView':
|
|
90
|
-
isCodeView.value = !isCodeView.value
|
|
91
|
-
break
|
|
92
|
-
case 'fullScreen':
|
|
93
|
-
toggleFullScreen()
|
|
94
|
-
break
|
|
95
|
-
case 'clear':
|
|
96
|
-
clearFormatting()
|
|
97
|
-
break
|
|
98
|
-
default:
|
|
99
|
-
applyFormatting(action, value)
|
|
100
|
-
}
|
|
101
|
-
updateContent()
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function toggleFullScreen() {
|
|
105
|
-
const editor = document.querySelector('.rich-text-editor')
|
|
106
|
-
editor?.classList.toggle('fullscreen-mode')
|
|
37
|
+
doc.body.focus()
|
|
107
38
|
}
|
|
108
39
|
|
|
109
40
|
watch(() => props.modelValue, (newValue) => {
|
|
110
|
-
if (newValue !==
|
|
111
|
-
|
|
112
|
-
if (editableContent.value) {
|
|
113
|
-
editableContent.value.innerHTML = newValue
|
|
114
|
-
}
|
|
41
|
+
if (newValue !== editor.state.content) {
|
|
42
|
+
editor.state.content = newValue
|
|
115
43
|
}
|
|
116
44
|
})
|
|
117
45
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
editableContent.value.innerHTML = contentHtml.value
|
|
121
|
-
}
|
|
122
|
-
document.addEventListener('selectionchange', updateToolbarHighlight)
|
|
46
|
+
watch(() => editor.state.content, (newValue) => {
|
|
47
|
+
emit('update:modelValue', newValue)
|
|
123
48
|
})
|
|
124
|
-
|
|
125
|
-
// Keyboard shortcuts
|
|
126
|
-
function handleKeyDown(event: KeyboardEvent) {
|
|
127
|
-
if (event.ctrlKey || event.metaKey) {
|
|
128
|
-
switch (event.key.toLowerCase()) {
|
|
129
|
-
case 'b':
|
|
130
|
-
event.preventDefault()
|
|
131
|
-
handleToolbarAction('bold')
|
|
132
|
-
break
|
|
133
|
-
case 'i':
|
|
134
|
-
event.preventDefault()
|
|
135
|
-
handleToolbarAction('italic')
|
|
136
|
-
break
|
|
137
|
-
case 'u':
|
|
138
|
-
event.preventDefault()
|
|
139
|
-
handleToolbarAction('underline')
|
|
140
|
-
break
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
49
|
</script>
|
|
145
50
|
|
|
146
51
|
<template>
|
|
147
|
-
<div class="rich-text-editor rounded pt-05 px-05 pb-1">
|
|
52
|
+
<div class="rich-text-editor rounded pt-05 px-05 pb-1" :class="{ 'fullscreen-mode': editor.state.isFullscreen }">
|
|
148
53
|
<Toolbar
|
|
149
|
-
:config="
|
|
150
|
-
|
|
151
|
-
@action="handleToolbarAction"
|
|
54
|
+
v-if="editor.state.hasInit" :config="toolbarConfig" :selectedStyles="editor.state.selectedStyles"
|
|
55
|
+
@action="editor.handleToolbarAction"
|
|
152
56
|
/>
|
|
153
|
-
<div class="editor-container" :class="{ 'split-view': isSplitView }">
|
|
154
|
-
<div class="content-area radius-05
|
|
155
|
-
<
|
|
156
|
-
v-if="isCodeView"
|
|
157
|
-
v-model="contentHtml"
|
|
158
|
-
class="html-editor"
|
|
159
|
-
@input="handleHtmlInput"
|
|
160
|
-
/>
|
|
161
|
-
<div
|
|
162
|
-
v-else
|
|
163
|
-
ref="editableContent"
|
|
164
|
-
contenteditable="true"
|
|
165
|
-
class="editableContent"
|
|
166
|
-
role="textbox"
|
|
167
|
-
aria-multiline="true"
|
|
168
|
-
tabindex="0"
|
|
169
|
-
@input="updateContent()"
|
|
170
|
-
@keydown="handleKeyDown"
|
|
171
|
-
/>
|
|
172
|
-
</div>
|
|
173
|
-
<div
|
|
174
|
-
v-if="isSplitView"
|
|
175
|
-
class="preview-area radius-05 p-1"
|
|
176
|
-
>
|
|
177
|
-
<pre><code v-text="contentHtml" /></pre>
|
|
57
|
+
<div class="editor-container" :class="{ 'split-view': editor?.state.isSplitView }">
|
|
58
|
+
<div class="content-area radius-05">
|
|
59
|
+
<iframe id="rich-text-iframe" ref="iframe" class="editableContent" title="Editor" @load="initEditor" />
|
|
178
60
|
</div>
|
|
61
|
+
<CodeEditor
|
|
62
|
+
v-if="editor?.state.isSplitView" v-model="editor.state.content" :languages="['html']"
|
|
63
|
+
@update:modelValue="editor.updateContent('html')"
|
|
64
|
+
/>
|
|
179
65
|
</div>
|
|
180
66
|
</div>
|
|
67
|
+
<pre wrap>
|
|
68
|
+
{{ editor.state }}
|
|
69
|
+
</pre>
|
|
181
70
|
</template>
|
|
182
71
|
|
|
183
72
|
<style scoped>
|
|
184
73
|
.rich-text-editor {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
74
|
+
background: var(--input-bg);
|
|
75
|
+
border: 1px solid var(--border-color);
|
|
76
|
+
transition: all 0.3s ease;
|
|
188
77
|
}
|
|
189
78
|
|
|
190
79
|
.editor-container {
|
|
191
|
-
|
|
192
|
-
|
|
80
|
+
display: flex;
|
|
81
|
+
gap: 1rem;
|
|
193
82
|
}
|
|
194
83
|
|
|
195
|
-
.content-area,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
84
|
+
.content-area,
|
|
85
|
+
.preview-area {
|
|
86
|
+
flex: 1;
|
|
87
|
+
min-height: 200px;
|
|
88
|
+
background: var(--bgl-richtext-color);
|
|
199
89
|
}
|
|
200
90
|
|
|
201
91
|
.split-view .content-area,
|
|
202
92
|
.split-view .preview-area {
|
|
203
|
-
|
|
93
|
+
width: 50%;
|
|
204
94
|
}
|
|
205
95
|
|
|
206
96
|
.editableContent {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
97
|
+
width: 100%;
|
|
98
|
+
min-height: 200px;
|
|
99
|
+
border: none;
|
|
100
|
+
outline: none;
|
|
101
|
+
background: transparent;
|
|
210
102
|
}
|
|
211
103
|
|
|
212
104
|
.html-editor {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
105
|
+
width: 100%;
|
|
106
|
+
height: 100%;
|
|
107
|
+
min-height: 200px;
|
|
108
|
+
font-family: monospace;
|
|
109
|
+
border: none;
|
|
110
|
+
outline: none;
|
|
111
|
+
resize: none;
|
|
112
|
+
color: white;
|
|
113
|
+
background-color: var(--bgl-black);
|
|
221
114
|
}
|
|
222
115
|
|
|
223
116
|
.preview-area {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.preview-area code {
|
|
230
|
-
display: block;
|
|
231
|
-
padding: 1rem;
|
|
117
|
+
font-family: monospace;
|
|
118
|
+
white-space: pre-wrap;
|
|
119
|
+
overflow-x: auto;
|
|
232
120
|
}
|
|
233
121
|
|
|
234
122
|
.fullscreen-mode {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
123
|
+
position: fixed;
|
|
124
|
+
top: 0;
|
|
125
|
+
left: 0;
|
|
126
|
+
width: 100vw;
|
|
127
|
+
height: 100vh;
|
|
128
|
+
z-index: 9999;
|
|
129
|
+
padding: 2rem;
|
|
242
130
|
}
|
|
243
131
|
|
|
244
132
|
.fullscreen-mode .editor-container {
|
|
245
|
-
|
|
133
|
+
height: calc(100vh - 8rem);
|
|
246
134
|
}
|
|
247
135
|
|
|
248
136
|
.fullscreen-mode .content-area,
|
|
249
137
|
.fullscreen-mode .preview-area {
|
|
250
|
-
|
|
251
|
-
|
|
138
|
+
height: 100%;
|
|
139
|
+
overflow-y: auto;
|
|
252
140
|
}
|
|
253
141
|
</style>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { MaterialIcons } from '@bagelink/vue'
|
|
2
|
+
|
|
3
|
+
export interface Modal {
|
|
4
|
+
showModalForm: (options: {
|
|
5
|
+
title: string
|
|
6
|
+
schema: any[]
|
|
7
|
+
onSubmit: (data: any) => void
|
|
8
|
+
}) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface EditorState {
|
|
12
|
+
isFullscreen: boolean
|
|
13
|
+
hasInit: boolean
|
|
14
|
+
isCodeView: boolean
|
|
15
|
+
isSplitView: boolean
|
|
16
|
+
selectedStyles: Set<string>
|
|
17
|
+
content: string
|
|
18
|
+
rangeCount: number
|
|
19
|
+
selection?: Selection | null
|
|
20
|
+
range?: Range | null
|
|
21
|
+
doc?: Document
|
|
22
|
+
modal?: Modal
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type FormattingCommand =
|
|
26
|
+
| 'bold'
|
|
27
|
+
| 'italic'
|
|
28
|
+
| 'underline'
|
|
29
|
+
| 'formatBlock'
|
|
30
|
+
| 'orderedList'
|
|
31
|
+
| 'unorderedList'
|
|
32
|
+
| 'link'
|
|
33
|
+
| 'image'
|
|
34
|
+
| 'youtube'
|
|
35
|
+
| 'table'
|
|
36
|
+
| 'splitView'
|
|
37
|
+
| 'codeView'
|
|
38
|
+
|
|
39
|
+
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
40
|
+
|
|
41
|
+
export type ToolbarConfigOption =
|
|
42
|
+
| FormattingCommand
|
|
43
|
+
| HeadingLevel
|
|
44
|
+
| 'separator'
|
|
45
|
+
| 'fullScreen'
|
|
46
|
+
| 'clear'
|
|
47
|
+
| 'mergeCells'
|
|
48
|
+
| 'splitCells'
|
|
49
|
+
| 'addRowBefore'
|
|
50
|
+
| 'addRowAfter'
|
|
51
|
+
| 'deleteRow'
|
|
52
|
+
| 'alignLeft'
|
|
53
|
+
| 'alignCenter'
|
|
54
|
+
| 'alignRight'
|
|
55
|
+
| 'alignJustify'
|
|
56
|
+
| 'indent'
|
|
57
|
+
| 'outdent'
|
|
58
|
+
| 'fontColor'
|
|
59
|
+
| 'bgColor'
|
|
60
|
+
| 'insertTable'
|
|
61
|
+
| 'deleteTable'
|
|
62
|
+
| 'insertRowAbove'
|
|
63
|
+
| 'insertRowBelow'
|
|
64
|
+
| 'insertColumnLeft'
|
|
65
|
+
| 'insertColumnRight'
|
|
66
|
+
| 'deleteColumn'
|
|
67
|
+
| 'undo'
|
|
68
|
+
| 'redo'
|
|
69
|
+
|
|
70
|
+
export type ToolbarConfig = ToolbarConfigOption[]
|
|
71
|
+
|
|
72
|
+
export interface ToolbarOption {
|
|
73
|
+
name: ToolbarConfigOption
|
|
74
|
+
label?: string
|
|
75
|
+
icon?: MaterialIcons
|
|
76
|
+
class?: string
|
|
77
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { EditorState } from '../richTextTypes'
|
|
2
|
+
|
|
3
|
+
export function formatting(state: EditorState) {
|
|
4
|
+
const text = (command: string) => {
|
|
5
|
+
const { doc, range, selection } = state
|
|
6
|
+
if (!doc || !range || !selection) return
|
|
7
|
+
let tag = ''
|
|
8
|
+
if (command === 'bold') tag = 'b'
|
|
9
|
+
if (command === 'italic') tag = 'i'
|
|
10
|
+
if (command === 'underline') tag = 'u'
|
|
11
|
+
if (command === 'emphasis') tag = 'em'
|
|
12
|
+
if (command === 'strong') tag = 'strong'
|
|
13
|
+
|
|
14
|
+
const relatedTags: { [key: string]: string[] } = {
|
|
15
|
+
b: ['strong'],
|
|
16
|
+
i: ['em'],
|
|
17
|
+
u: [],
|
|
18
|
+
em: ['i'],
|
|
19
|
+
strong: ['b']
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const selectedContent = range.extractContents()
|
|
23
|
+
const wrapper = doc.createElement(tag)
|
|
24
|
+
wrapper.appendChild(selectedContent)
|
|
25
|
+
const container = range.commonAncestorContainer
|
|
26
|
+
const parent = container.parentElement
|
|
27
|
+
|
|
28
|
+
if (parent && (parent.tagName.toLowerCase() === tag || relatedTags[tag].includes(parent.tagName.toLowerCase()))) {
|
|
29
|
+
while (wrapper.firstChild) {
|
|
30
|
+
parent.parentNode?.insertBefore(wrapper.firstChild, parent)
|
|
31
|
+
}
|
|
32
|
+
parent.parentNode?.removeChild(parent)
|
|
33
|
+
} else {
|
|
34
|
+
range.insertNode(wrapper)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const nestedTags = wrapper.querySelectorAll(`${tag},${(relatedTags[tag] || []).join(',')}`)
|
|
38
|
+
nestedTags.forEach((nestedTag) => {
|
|
39
|
+
while (nestedTag.firstChild) {
|
|
40
|
+
nestedTag.parentNode?.insertBefore(nestedTag.firstChild, nestedTag)
|
|
41
|
+
}
|
|
42
|
+
nestedTag.parentNode?.removeChild(nestedTag)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
selection.removeAllRanges()
|
|
46
|
+
selection.addRange(range)
|
|
47
|
+
}
|
|
48
|
+
const block = (command: string, value: string) => {
|
|
49
|
+
const { doc, range, selection } = state
|
|
50
|
+
if (!doc || !range || !selection) return
|
|
51
|
+
const container = range.commonAncestorContainer
|
|
52
|
+
const parent = container.nodeType === 3 ? container.parentElement : (container as Element)
|
|
53
|
+
if (!parent) return
|
|
54
|
+
|
|
55
|
+
const currentHeading = parent.closest('p,h1,h2,h3,h4,h5,h6')
|
|
56
|
+
const { startOffset, endOffset } = range
|
|
57
|
+
const blockEl = currentHeading || parent.closest('p,h1,h2,h3,h4,h5,h6') || parent
|
|
58
|
+
range.selectNodeContents(blockEl)
|
|
59
|
+
|
|
60
|
+
const content = range.extractContents()
|
|
61
|
+
const newElement = doc.createElement(currentHeading && currentHeading.tagName.toLowerCase() === value ? 'p' : value)
|
|
62
|
+
newElement.appendChild(content)
|
|
63
|
+
|
|
64
|
+
if (currentHeading) {
|
|
65
|
+
currentHeading.parentNode?.replaceChild(newElement, currentHeading)
|
|
66
|
+
} else {
|
|
67
|
+
range.insertNode(newElement)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
selection.removeAllRanges()
|
|
71
|
+
range.setStart(container, startOffset)
|
|
72
|
+
range.setEnd(container, endOffset)
|
|
73
|
+
selection.addRange(range)
|
|
74
|
+
}
|
|
75
|
+
const list = (command: string) => {
|
|
76
|
+
const { doc, range } = state
|
|
77
|
+
if (!doc || !range) return
|
|
78
|
+
const listTag = command === 'orderedList' ? 'ol' : 'ul'
|
|
79
|
+
const container = range.commonAncestorContainer
|
|
80
|
+
const parent = container.nodeType === 3 ? container.parentElement : (container as Element)
|
|
81
|
+
const currentList = parent?.closest('ul,ol')
|
|
82
|
+
|
|
83
|
+
if (currentList) {
|
|
84
|
+
const content = range.extractContents()
|
|
85
|
+
const p = doc.createElement('p')
|
|
86
|
+
p.appendChild(content)
|
|
87
|
+
currentList.parentNode?.replaceChild(p, currentList)
|
|
88
|
+
} else {
|
|
89
|
+
const listEl = doc.createElement(listTag)
|
|
90
|
+
const li = doc.createElement('li')
|
|
91
|
+
const content = range.extractContents()
|
|
92
|
+
li.appendChild(content)
|
|
93
|
+
listEl.appendChild(li)
|
|
94
|
+
range.insertNode(listEl)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { text, block, list }
|
|
98
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Modal } from '../richTextTypes'
|
|
2
|
+
|
|
3
|
+
export function insertImage(modal: Modal, doc: Document, range: Range | null) {
|
|
4
|
+
if (!range) return
|
|
5
|
+
|
|
6
|
+
modal.showModalForm({
|
|
7
|
+
title: 'Upload Image',
|
|
8
|
+
schema: [
|
|
9
|
+
{ id: 'src', $el: 'file', attrs: { bindkey: 'url' } },
|
|
10
|
+
{ id: 'alt', $el: 'text', label: 'Alt Text' },
|
|
11
|
+
],
|
|
12
|
+
onSubmit: (data: { src: string, alt: string }) => {
|
|
13
|
+
if (data.src) {
|
|
14
|
+
const img = doc.createElement('img')
|
|
15
|
+
img.src = data.src
|
|
16
|
+
img.alt = data.alt
|
|
17
|
+
range.deleteContents()
|
|
18
|
+
range.insertNode(img)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function insertLink(modal: Modal, doc: Document, range: Range | null) {
|
|
25
|
+
if (!range) return
|
|
26
|
+
|
|
27
|
+
modal.showModalForm({
|
|
28
|
+
title: 'Insert Link',
|
|
29
|
+
schema: [
|
|
30
|
+
{ id: 'url', $el: 'text', label: 'URL' },
|
|
31
|
+
{ id: 'openInNewTab', $el: 'check', label: 'Open in new tab' },
|
|
32
|
+
],
|
|
33
|
+
onSubmit: (data: { url: string, openInNewTab: boolean }) => {
|
|
34
|
+
if (data.url) {
|
|
35
|
+
const anchor = doc.createElement('a')
|
|
36
|
+
anchor.href = data.url
|
|
37
|
+
if (data.openInNewTab) anchor.target = '_blank'
|
|
38
|
+
range.surroundContents(anchor)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
export function getSelection(state: any) {
|
|
3
|
+
return state.selection
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function getSelectionRange(state: any) {
|
|
7
|
+
return state.range
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isStyleActive(style: string, doc: Document) {
|
|
11
|
+
const selection = doc.getSelection()
|
|
12
|
+
if (!selection || !selection.rangeCount) return false
|
|
13
|
+
|
|
14
|
+
const range = selection.getRangeAt(0)
|
|
15
|
+
const container = range.commonAncestorContainer
|
|
16
|
+
const parent = container.nodeType === 3 ? container.parentElement : container as Element
|
|
17
|
+
|
|
18
|
+
if (!parent) return false
|
|
19
|
+
|
|
20
|
+
const checkParent = (element: Element | null, tags: string[]): boolean => {
|
|
21
|
+
if (!element) return false
|
|
22
|
+
if (tags.includes(element.tagName.toLowerCase())) return true
|
|
23
|
+
return checkParent(element.parentElement, tags)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
switch (style) {
|
|
27
|
+
case 'bold':
|
|
28
|
+
return checkParent(parent, ['strong', 'b'])
|
|
29
|
+
case 'italic':
|
|
30
|
+
return checkParent(parent, ['em', 'i'])
|
|
31
|
+
case 'underline':
|
|
32
|
+
return checkParent(parent, ['u'])
|
|
33
|
+
case 'h1':
|
|
34
|
+
return checkParent(parent, ['h1'])
|
|
35
|
+
case 'h2':
|
|
36
|
+
return checkParent(parent, ['h2'])
|
|
37
|
+
case 'h3':
|
|
38
|
+
return checkParent(parent, ['h3'])
|
|
39
|
+
case 'h4':
|
|
40
|
+
return checkParent(parent, ['h4'])
|
|
41
|
+
case 'h5':
|
|
42
|
+
return checkParent(parent, ['h5'])
|
|
43
|
+
case 'h6':
|
|
44
|
+
return checkParent(parent, ['h6'])
|
|
45
|
+
default:
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
|
|
2
|
+
export function createTable(rows: number, cols: number, doc: Document) {
|
|
3
|
+
const table = doc.createElement('table')
|
|
4
|
+
table.style.width = '100%'
|
|
5
|
+
table.style.borderCollapse = 'collapse'
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < rows; i++) {
|
|
8
|
+
const row = table.insertRow()
|
|
9
|
+
for (let j = 0; j < cols; j++) {
|
|
10
|
+
const cell = row.insertCell()
|
|
11
|
+
cell.style.border = '1px solid var(--border-color)'
|
|
12
|
+
cell.style.padding = '8px'
|
|
13
|
+
cell.innerHTML = ' '
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return table
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mergeCells(range: Range, doc: Document) {
|
|
21
|
+
const cells = Array.from(range.cloneContents().querySelectorAll('td'))
|
|
22
|
+
if (cells.length < 2) return
|
|
23
|
+
|
|
24
|
+
const firstCell = range.startContainer.parentElement?.closest('td')
|
|
25
|
+
if (!firstCell) return
|
|
26
|
+
|
|
27
|
+
firstCell.colSpan = cells.length
|
|
28
|
+
firstCell.innerHTML = cells.map(cell => cell.innerHTML).join(' ')
|
|
29
|
+
|
|
30
|
+
cells.slice(1).forEach((cell) => {
|
|
31
|
+
const actualCell = doc.getElementById(cell.id)
|
|
32
|
+
actualCell?.remove()
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function splitCell(range: Range, doc: Document) {
|
|
37
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
38
|
+
if (!cell || !cell.colSpan || cell.colSpan === 1) return
|
|
39
|
+
|
|
40
|
+
const newCells = new Array(cell.colSpan - 1).fill(0).map(() => {
|
|
41
|
+
const newCell = doc.createElement('td')
|
|
42
|
+
newCell.style.border = '1px solid var(--border-color)'
|
|
43
|
+
newCell.style.padding = '8px'
|
|
44
|
+
newCell.innerHTML = ' '
|
|
45
|
+
return newCell
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
cell.colSpan = 1
|
|
49
|
+
cell.insertAdjacentElement('afterend', newCells[0])
|
|
50
|
+
newCells.slice(1).forEach((newCell) => {
|
|
51
|
+
newCells[0].insertAdjacentElement('afterend', newCell)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function addRow(position: 'before' | 'after', range: Range, doc: Document) {
|
|
56
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
57
|
+
if (!cell) return
|
|
58
|
+
|
|
59
|
+
const row = cell.parentElement
|
|
60
|
+
const table = row?.parentElement
|
|
61
|
+
if (!row || !table) return
|
|
62
|
+
|
|
63
|
+
const newRow = row.cloneNode(true) as HTMLTableRowElement
|
|
64
|
+
Array.from(newRow.cells).forEach((cell) => {
|
|
65
|
+
cell.innerHTML = ' '
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
row.insertAdjacentElement(position === 'before' ? 'beforebegin' : 'afterend', newRow)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function deleteRow(range: Range) {
|
|
72
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
73
|
+
if (!cell) return
|
|
74
|
+
|
|
75
|
+
const row = cell.parentElement
|
|
76
|
+
if (!row) return
|
|
77
|
+
|
|
78
|
+
row.remove()
|
|
79
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as Checkbox } from './Checkbox.vue'
|
|
2
2
|
export { default as CheckInput } from './CheckInput.vue'
|
|
3
|
+
export { default as CodeEditor } from './CodeEditor/Index.vue'
|
|
3
4
|
export { default as ColorPicker } from './ColorPicker.vue'
|
|
4
5
|
export { default as DateInput } from './DateInput.vue'
|
|
5
6
|
export { default as DatePicker } from './DatePicker.vue'
|