@bagelink/vue 0.0.986 → 0.0.992
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/Loading.vue.d.ts +16 -0
- package/dist/components/Loading.vue.d.ts.map +1 -0
- package/dist/components/form/BglField.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 +15 -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/FileUpload.vue.d.ts.map +1 -1
- 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 +7 -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 +7 -0
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -0
- package/dist/components/form/inputs/index.d.ts +1 -0
- package/dist/components/form/inputs/index.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/TabsNav.vue.d.ts.map +1 -1
- package/dist/editor-CUDRLdmS.js +4 -0
- package/dist/editor-Cu374vEW.cjs +4 -0
- package/dist/editor-a8DSbb6P.js +4 -0
- package/dist/editor-xBt_vIha.cjs +4 -0
- package/dist/index.cjs +15594 -1103
- package/dist/index.mjs +15595 -1104
- package/dist/style.css +790 -511
- package/package.json +6 -33
- package/src/components/Btn.vue +113 -136
- package/src/components/Loading.vue +177 -0
- package/src/components/form/BglField.vue +2 -0
- package/src/components/form/inputs/CodeEditor/CodeTypes.ts +446 -0
- package/src/components/form/inputs/CodeEditor/Index.vue +117 -0
- package/src/components/form/inputs/CodeEditor/format.ts +98 -0
- package/src/components/form/inputs/FileUpload.vue +2 -1
- package/src/components/form/inputs/RichText/components/Toolbar.vue +51 -0
- package/src/components/form/inputs/RichText/components/gridBox.vue +51 -0
- package/src/components/form/inputs/RichText/composables/useEditor.ts +215 -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 +84 -195
- 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 +81 -0
- package/src/components/form/inputs/index.ts +1 -0
- package/src/components/index.ts +2 -2
- package/src/components/layout/TabsNav.vue +1 -0
- package/src/styles/theme.css +256 -256
- 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
- package/src/components/formkit/FileUploader.vue +0 -406
- package/src/components/formkit/MiscFields.vue +0 -74
- package/src/components/formkit/Toggle.vue +0 -149
|
@@ -1,253 +1,142 @@
|
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
switch (action) {
|
|
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
|
-
}
|
|
32
|
+
editor.init(doc, modal)
|
|
33
|
+
useEditorKeyboard(doc, (action, value) => {
|
|
34
|
+
editor.handleToolbarAction(action, value)
|
|
35
|
+
})
|
|
103
36
|
|
|
104
|
-
|
|
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-
|
|
52
|
+
<div class="rich-text-editor rounded pt-05 px-05 pb-075" :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" language="html"
|
|
63
|
+
@update:modelValue="editor.updateContent('html')"
|
|
64
|
+
/>
|
|
179
65
|
</div>
|
|
180
66
|
</div>
|
|
181
67
|
</template>
|
|
182
68
|
|
|
183
69
|
<style scoped>
|
|
184
70
|
.rich-text-editor {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
71
|
+
background: var(--input-bg);
|
|
72
|
+
border: 1px solid var(--border-color);
|
|
73
|
+
transition: all 0.3s ease;
|
|
188
74
|
}
|
|
189
75
|
|
|
190
76
|
.editor-container {
|
|
191
|
-
|
|
192
|
-
|
|
77
|
+
display: flex;
|
|
78
|
+
gap: 1rem;
|
|
193
79
|
}
|
|
194
80
|
|
|
195
|
-
.content-area,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
81
|
+
.content-area,
|
|
82
|
+
.preview-area {
|
|
83
|
+
flex: 1;
|
|
84
|
+
min-height: 200px;
|
|
85
|
+
background: var(--bgl-richtext-color);
|
|
199
86
|
}
|
|
200
87
|
|
|
201
|
-
.split-view
|
|
202
|
-
|
|
203
|
-
|
|
88
|
+
.split-view {
|
|
89
|
+
display: grid;
|
|
90
|
+
grid-template-columns: 1fr 1fr;
|
|
204
91
|
}
|
|
205
92
|
|
|
206
93
|
.editableContent {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
94
|
+
width: 100%;
|
|
95
|
+
min-height: 240px;
|
|
96
|
+
height: 100%;
|
|
97
|
+
border: none;
|
|
98
|
+
outline: none;
|
|
99
|
+
background: transparent;
|
|
210
100
|
}
|
|
211
101
|
|
|
212
102
|
.html-editor {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
103
|
+
width: 100%;
|
|
104
|
+
height: 100%;
|
|
105
|
+
min-height: 200px;
|
|
106
|
+
font-family: monospace;
|
|
107
|
+
border: none;
|
|
108
|
+
outline: none;
|
|
109
|
+
resize: none;
|
|
110
|
+
color: white;
|
|
111
|
+
background-color: var(--bgl-black);
|
|
221
112
|
}
|
|
222
113
|
|
|
223
114
|
.preview-area {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.preview-area code {
|
|
230
|
-
display: block;
|
|
231
|
-
padding: 1rem;
|
|
115
|
+
font-family: monospace;
|
|
116
|
+
white-space: pre-wrap;
|
|
117
|
+
overflow-x: auto;
|
|
232
118
|
}
|
|
233
119
|
|
|
234
120
|
.fullscreen-mode {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
121
|
+
position: fixed;
|
|
122
|
+
top: 0;
|
|
123
|
+
left: 0;
|
|
124
|
+
width: 100vw;
|
|
125
|
+
height: 100vh;
|
|
126
|
+
z-index: 9999;
|
|
127
|
+
padding: 2rem;
|
|
242
128
|
}
|
|
243
129
|
|
|
244
130
|
.fullscreen-mode .editor-container {
|
|
245
|
-
|
|
131
|
+
height: calc(100vh - 4rem);
|
|
246
132
|
}
|
|
247
133
|
|
|
248
134
|
.fullscreen-mode .content-area,
|
|
249
135
|
.fullscreen-mode .preview-area {
|
|
250
|
-
|
|
251
|
-
|
|
136
|
+
height: 100%;
|
|
137
|
+
overflow-y: auto;
|
|
138
|
+
}
|
|
139
|
+
.fullscreen-mode .code-editor{
|
|
140
|
+
height: 100% !important;
|
|
252
141
|
}
|
|
253
142
|
</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,81 @@
|
|
|
1
|
+
import type { EditorState } from '../richTextTypes'
|
|
2
|
+
|
|
3
|
+
export function insertTable(rows: number, cols: number, state: EditorState) {
|
|
4
|
+
if (!state.doc) return
|
|
5
|
+
const table = state.doc.createElement('table')
|
|
6
|
+
table.style.width = '100%'
|
|
7
|
+
table.style.borderCollapse = 'collapse'
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < rows; i++) {
|
|
10
|
+
const row = table.insertRow()
|
|
11
|
+
for (let j = 0; j < cols; j++) {
|
|
12
|
+
const cell = row.insertCell()
|
|
13
|
+
cell.innerHTML = ' '
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const { range } = state
|
|
17
|
+
if (range) {
|
|
18
|
+
range.insertNode(table)
|
|
19
|
+
} else {
|
|
20
|
+
state.doc.body.appendChild(table)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mergeCells(range: Range, doc: Document) {
|
|
25
|
+
const cells = Array.from(range.cloneContents().querySelectorAll('td'))
|
|
26
|
+
if (cells.length < 2) return
|
|
27
|
+
|
|
28
|
+
const firstCell = range.startContainer.parentElement?.closest('td')
|
|
29
|
+
if (!firstCell) return
|
|
30
|
+
|
|
31
|
+
firstCell.colSpan = cells.length
|
|
32
|
+
firstCell.innerHTML = cells.map(cell => cell.innerHTML).join(' ')
|
|
33
|
+
|
|
34
|
+
cells.slice(1).forEach((cell) => {
|
|
35
|
+
const actualCell = doc.getElementById(cell.id)
|
|
36
|
+
actualCell?.remove()
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function splitCell(range: Range, doc: Document) {
|
|
41
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
42
|
+
if (!cell || !cell.colSpan || cell.colSpan === 1) return
|
|
43
|
+
|
|
44
|
+
const newCells = new Array(cell.colSpan - 1).fill(0).map(() => {
|
|
45
|
+
const newCell = doc.createElement('td')
|
|
46
|
+
newCell.style.border = '1px solid var(--border-color)'
|
|
47
|
+
newCell.style.padding = '8px'
|
|
48
|
+
newCell.innerHTML = ' '
|
|
49
|
+
return newCell
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
cell.colSpan = 1
|
|
53
|
+
cell.insertAdjacentElement('afterend', newCells[0])
|
|
54
|
+
newCells.slice(1).forEach((newCell) => {
|
|
55
|
+
newCells[0].insertAdjacentElement('afterend', newCell)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function addRow(position: 'before' | 'after', range: Range, doc: Document) {
|
|
60
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
61
|
+
if (!cell) return
|
|
62
|
+
|
|
63
|
+
const row = cell.parentElement
|
|
64
|
+
const table = row?.parentElement
|
|
65
|
+
if (!row || !table) return
|
|
66
|
+
|
|
67
|
+
const newRow = row.cloneNode(true) as HTMLTableRowElement
|
|
68
|
+
Array.from(newRow.cells).forEach(cell => cell.innerHTML = ' ')
|
|
69
|
+
|
|
70
|
+
row.insertAdjacentElement(position === 'before' ? 'beforebegin' : 'afterend', newRow)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function deleteRow(range: Range) {
|
|
74
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
75
|
+
if (!cell) return
|
|
76
|
+
|
|
77
|
+
const row = cell.parentElement
|
|
78
|
+
if (!row) return
|
|
79
|
+
|
|
80
|
+
row.remove()
|
|
81
|
+
}
|
|
@@ -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'
|